Service Layer Architecture
Purpose and Scope
This document explains the Service Layer pattern used throughout the Agentic Browser backend. Services encapsulate business logic, coordinate between API routers and tool modules, and provide consistent error handling and logging. This layer sits between the FastAPI routers (documented in 3.3) and the reusable tool modules (documented in 3.5).
For information about agent-specific service orchestration, see the React Agent Architecture (4.1).
Service Layer Role
The service layer implements a three-tier architecture pattern:

Sources: services/github_service.py1-40 services/website_service.py1-53 services/youtube_service.py1-35 services/google_search_service.py1-31 services/gmail_service.py1-56 services/calendar_service.py1-38 services/pyjiit_service.py1-125 services/react_agent_service.py1-92
Separation of Concerns
| Layer | Responsibility | Example Code Entity |
|---|---|---|
| Router | HTTP request/response handling, input validation, authentication checks | @router.post("/answer") |
| Service | Business logic orchestration, error handling, logging, data transformation | GitHubService.generate_answer() |
| Tool | Reusable operations, external API calls, data processing | convert_github_repo_to_markdown() |
This separation enables:
- Testability: Services can be unit tested independently of FastAPI infrastructure
- Reusability: Tools can be shared across services and agent tools
- Maintainability: Changes to business logic don't affect HTTP handling
- Composability: Services coordinate multiple tools for complex workflows
Service Class Structure
Common Service Pattern
All services follow a consistent structure:

Sources: services/github_service.py10-40 services/website_service.py8-53 services/youtube_service.py7-35 services/google_search_service.py7-31 services/gmail_service.py10-56 services/calendar_service.py8-38 services/pyjiit_service.py13-125 services/react_agent_service.py14-92
Service Categories
Services can be categorized by their interaction patterns:
| Category | Services | Tool Integration | LLM Integration |
|---|---|---|---|
| LLM-Driven Q&A | GitHubService, WebsiteService, YouTubeService |
Content extraction tools | Prompt chains with get_chain() or similar |
| Direct API Wrapper | GmailService, CalendarService, GoogleSearchService |
Tool functions that wrap external APIs | None (agent tools use LLM separately) |
| Session Management | PyjiitService |
Custom API client with encryption | None |
| Agent Orchestration | ReactAgentService |
Agent tools via GraphBuilder |
LLM called by LangGraph |
Error Handling and Logging
Structured Logging Pattern
All services obtain a logger using the get_logger(__name__) pattern from the core module:

Example: GitHubService logging
- services/github_service.py3 imports
get_logger - services/github_service.py7 creates logger:
logger = get_logger(__name__) - services/github_service.py38 logs errors:
logger.error(f"Error generating answer with LLM: {e}")
Example: ReactAgentService logging
- services/react_agent_service.py8 imports
get_logger - services/react_agent_service.py11 creates logger
- services/react_agent_service.py63-73 logs detailed message history with structured info
- services/react_agent_service.py82 logs response content
- services/react_agent_service.py86 logs exceptions
Sources: services/github_service.py3-38 services/website_service.py1-51 services/youtube_service.py1-33 services/google_search_service.py1-29 services/gmail_service.py1-54 services/calendar_service.py1-36 services/pyjiit_service.py4-123 services/react_agent_service.py8-86
Exception Handling Pattern
Services implement a consistent try/except pattern:

Q&A Services (GitHub, Website, YouTube) return graceful error messages:
- services/github_service.py37-39: Returns
"I apologize, but I encountered an error processing your question about the GitHub repository. Please try again." - services/website_service.py50-52: Returns
"I apologize, but I encountered an error processing your question. Please try again." - services/youtube_service.py32-34: Returns
"I apologize, but I encountered an error processing your question about the video. Please try again." - services/react_agent_service.py85-91: Returns
"I apologize, but I encountered an error processing your question. Please try again."
API Wrapper Services (Gmail, Calendar, GoogleSearch, PyJIIT) propagate exceptions to routers:
- services/gmail_service.py17-20:
logger.exception()thenraise - services/calendar_service.py15-17:
logger.exception()thenraise - services/google_search_service.py28-30:
logger.exception()thenraise - services/pyjiit_service.py20-22:
logger.exception()thenraise
Sources: services/github_service.py37-39 services/website_service.py50-52 services/youtube_service.py32-34 services/react_agent_service.py85-91 services/gmail_service.py17-20 services/calendar_service.py15-17 services/google_search_service.py28-30 services/pyjiit_service.py20-22
Integration with Tool Layer
Services delegate specific operations to tool modules, maintaining a clear dependency hierarchy:

Sources: services/github_service.py5-19 services/website_service.py3-22 services/youtube_service.py2-17 services/google_search_service.py2-15 services/gmail_service.py2-50 services/calendar_service.py2-34 services/pyjiit_service.py8-60
Service-to-Tool Call Patterns
| Service | Tool Import | Tool Function Call | Line Reference |
|---|---|---|---|
GitHubService |
from tools.github_crawler import convert_github_repo_to_markdown |
await convert_github_repo_to_markdown(url) |
services/github_service.py5-19 |
WebsiteService |
from tools.website_context import markdown_fetcher |
markdown_fetcher(url) |
services/website_service.py3-22 |
YouTubeService |
from prompts.youtube import youtube_chain |
youtube_chain.invoke({...}) |
services/youtube_service.py2-17 |
GoogleSearchService |
from tools.google_search.seach_agent import web_search_pipeline |
web_search_pipeline(query, max_results=max_results) |
services/google_search_service.py2-15 |
GmailService |
from tools.gmail.list_unread_emails import list_unread |
list_unread(access_token, max_results=max_results) |
services/gmail_service.py3-13 |
CalendarService |
from tools.calendar.get_calender_events import get_calendar_events |
get_calendar_events(access_token, max_results=max_results) |
services/calendar_service.py3-11 |
PyjiitService |
from tools.pyjiit.wrapper import Webportal, WebportalSession |
Webportal().student_login(...) |
services/pyjiit_service.py8-16 |
Integration with Prompt and LLM Layer
Q&A-oriented services integrate with prompt chains to generate responses:

Sources: services/github_service.py4-30 services/website_service.py2-43 services/youtube_service.py2-23
Chain Construction and Initialization
GitHubService: Obtains chain on-demand per request
- services/github_service.py21:
chain = get_chain()called withingenerate_answer() - services/github_service.py22-30: Invokes chain with
summary,tree,text,question,chat_historyparameters
WebsiteService: Constructs chain during initialization
- services/website_service.py9-10:
__init__()createsself.chain = get_chain() - services/website_service.py38-43: Uses
get_answer(self.chain, question, markdown_page_info, chat_history_str)helper function
YouTubeService: Uses pre-built chain module
- services/youtube_service.py2: Imports
youtube_chaindirectly - services/youtube_service.py17-23: Invokes with
url,question,chat_historyparameters
Response Handling Pattern
All LLM-driven services handle responses consistently:
# Pattern: Check if response is string or has .content attribute
if isinstance(response, str):
return response
return response.content
- services/github_service.py32-35: Implements this pattern
- services/website_service.py45-48: Implements this pattern
- services/youtube_service.py27-30: Implements this pattern
Sources: services/github_service.py32-35 services/website_service.py45-48 services/youtube_service.py27-30
Context Management and Authentication
Services handle authentication context differently based on their requirements:

Sources: services/react_agent_service.py18-34 services/gmail_service.py11-47 services/calendar_service.py9-27 services/pyjiit_service.py14-124
ReactAgentService Context Building
ReactAgentService builds a context dictionary for conditional tool availability:

Implementation:
- services/react_agent_service.py23: Initialize empty context dict
- services/react_agent_service.py25-26: Add
google_access_tokenif provided - services/react_agent_service.py28-34: Add
pyjiit_login_response, handling both Pydantic models and dicts - services/react_agent_service.py36: Pass context to
GraphBuilder
The GraphBuilder uses this context to determine which tools to make available in the agent's tool list. For example, Gmail and Calendar tools are only available when google_access_token is present.
Sources: services/react_agent_service.py23-36
API Wrapper Services Authentication
Services that wrap external APIs accept authentication tokens as method parameters:
GmailService:
- services/gmail_service.py11:
list_unread_messages(access_token, ...) - services/gmail_service.py22:
fetch_latest_messages(access_token, ...) - services/gmail_service.py33:
mark_message_read(access_token, ...) - services/gmail_service.py44:
send_message(access_token, ...)
CalendarService:
- services/calendar_service.py9:
list_events(access_token, ...) - services/calendar_service.py19:
create_event(access_token, ...)
PyjiitService Session Management:
- services/pyjiit_service.py14:
login()returnssession.model_dump()as dict - services/pyjiit_service.py24-44:
get_semesters()acceptssession_payloaddict and reconstructsWebportalSession - services/pyjiit_service.py46-124:
get_attendance()handles nested dict structures with"session_payload"and"raw_response"keys
Sources: services/gmail_service.py11-44 services/calendar_service.py9-19 services/pyjiit_service.py14-60
Common Service Patterns
Asynchronous vs Synchronous Methods
Services use async methods when calling async tools:
| Service | Method Type | Reason |
|---|---|---|
GitHubService.generate_answer() |
async |
Calls await convert_github_repo_to_markdown() |
WebsiteService.generate_answer() |
async |
Chain invocation may be async |
YouTubeService.generate_answer() |
async |
Chain invocation may be async |
ReactAgentService.generate_answer() |
async |
Calls await graph.ainvoke() |
GmailService.* |
Synchronous | Wraps synchronous tool functions |
CalendarService.* |
Synchronous | Wraps synchronous tool functions |
GoogleSearchService.search() |
Synchronous | Wraps synchronous tool functions |
PyjiitService.* |
Synchronous | Wraps synchronous tool functions |
Sources: services/github_service.py11-16 services/website_service.py12-17 services/youtube_service.py8-13 services/react_agent_service.py15-21 services/gmail_service.py11-44 services/calendar_service.py9-19 services/google_search_service.py8 services/pyjiit_service.py14-46
Parameter Consistency
Services maintain consistent parameter naming:
Common Parameters:
url: String orHttpUrlfor web resourcesquestion: User query stringchat_history: List of previous messages (format varies by service)access_token: OAuth token for Google servicesmax_results: Result count limit (default varies)
Chat History Formats:
GitHubService:list[dict]with default[](services/github_service.py15)WebsiteService:listof any format (services/website_service.py16)YouTubeService:strwith default""(services/youtube_service.py12)ReactAgentService:list[dict[str, Any]] | None(services/react_agent_service.py18)
Data Transformation Patterns
Chat History Conversion in ReactAgentService:
services/react_agent_service.py38-56 converts dict-based chat history to LangChain message objects:
messages_list: list = []
if chat_history:
for entry in chat_history:
if isinstance(entry, dict):
role = (entry.get("role") or "").lower()
content = entry.get("content", "")
if role == "user":
messages_list.append(HumanMessage(content=content))
elif role in {"assistant", "bot", "ai"}:
messages_list.append(AIMessage(content=content))
Chat History String Building in WebsiteService:
services/website_service.py28-36 concatenates chat history into a string:
chat_history_str = ""
if chat_history:
for entry in chat_history:
if isinstance(entry, dict):
role = entry.get("role", "")
content = entry.get("content", "")
chat_history_str += f"{role}: {content}\n"
PyjiitService Data Processing:
services/pyjiit_service.py104-118 extracts and cleans attendance data using regex:
for item in raw_list:
subj = item.get("subjectcode", "") or ""
# Extract code in parentheses
m = re.search(r"\(([^)]+)\)\s*$", subj)
code = m.group(1) if m else ""
# Strip trailing bracketed code
subject_no_bracket = re.sub(r"\s*\([^)]*\)\s*$", "", subj).strip()
Sources: services/react_agent_service.py38-56 services/website_service.py28-36 services/pyjiit_service.py104-118
Return Value Patterns
Services return different data structures based on their purpose:
| Service | Return Type | Example Value |
|---|---|---|
| Q&A Services | str |
Answer text or error message |
GoogleSearchService |
List of search results | [{...}] from Tavily |
GmailService.list_unread_messages() |
List of email dicts | [{"id": ..., "subject": ...}] |
CalendarService.list_events() |
List of event dicts | [{"summary": ..., "start": ...}] |
PyjiitService.login() |
Dict[str, Any] |
Serialized WebportalSession |
PyjiitService.get_attendance() |
List[Dict[str, Any]] |
Attendance records |
Service Instantiation and Dependency Injection
Services are instantiated by routers, typically as singletons or per-request instances:

Most services are stateless and can be instantiated without parameters:
service = GitHubService()
service = YouTubeService()
service = GoogleSearchService()
# etc.
Exception: WebsiteService
services/website_service.py9-10 initializes a chain during construction:
def __init__(self):
self.chain = get_chain()
This allows reusing the same chain instance across multiple requests, improving performance.
Sources: All service files show parameter-free constructors except services/website_service.py9-10