首次提交

This commit is contained in:
lhr
2026-01-19 22:04:54 +08:00
parent 1fa5a4947a
commit 12ef0292b7
16 changed files with 1147 additions and 0 deletions

5
app/services/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .session_service import (
init_session_service,
close_session_service,
get_session_service
)

View File

@@ -0,0 +1,89 @@
import uuid
import time
from typing import Dict, List, Optional
import logging
from app.config import settings
from app.models import AgentConfig, AgentDefinition
logger = logging.getLogger(__name__)
class AgentService:
"""
Service for managing Agent Definitions.
Currently uses in-memory storage.
Can be extended to support Database storage.
"""
def __init__(self):
# In-memory store: {agent_id: AgentDefinition}
self._store: Dict[str, AgentDefinition] = {}
# Populate with a default agent for convenience
self._create_default_agent()
def _create_default_agent(self):
default_id = "default-assistant"
if default_id not in self._store:
self._store[default_id] = AgentDefinition(
id=default_id,
name="Assistant",
description="A helpful default AI assistant",
model=settings.LLM_DEFAULT_MODEL,
instruction="You are a helpful assistant.",
tools=[],
created_at=time.time(),
updated_at=time.time()
)
def create_agent(self, config: AgentConfig) -> AgentDefinition:
"""Create and store a new agent definition."""
agent_id = str(uuid.uuid4())
definition = AgentDefinition(
id=agent_id,
**config.model_dump(),
created_at=time.time(),
updated_at=time.time()
)
self._store[agent_id] = definition
logger.info(f"Created new agent definition: {agent_id}")
return definition
def get_agent(self, agent_id: str) -> Optional[AgentDefinition]:
"""Retrieve an agent definition by ID."""
return self._store.get(agent_id)
def update_agent(self, agent_id: str, config: AgentConfig) -> Optional[AgentDefinition]:
"""Update an existing agent definition."""
if agent_id not in self._store:
return None
# Preserve original creation time and ID
original = self._store[agent_id]
updated = AgentDefinition(
id=agent_id,
**config.model_dump(),
created_at=original.created_at,
updated_at=time.time()
)
self._store[agent_id] = updated
logger.info(f"Updated agent definition: {agent_id}")
return updated
def list_agents(self) -> List[AgentDefinition]:
"""List all stored agents."""
return list(self._store.values())
def delete_agent(self, agent_id: str) -> bool:
"""Delete an agent definition."""
if agent_id in self._store:
del self._store[agent_id]
logger.info(f"Deleted agent definition: {agent_id}")
return True
return False
# Singleton instance
_agent_service = AgentService()
def get_agent_service() -> AgentService:
return _agent_service

View File

@@ -0,0 +1,40 @@
import logging
from google.adk.sessions import DatabaseSessionService
from app.config import settings
logger = logging.getLogger(__name__)
# Global service instance
_session_service: DatabaseSessionService | None = None
async def init_session_service() -> None:
"""Initialize the database session service on application startup."""
global _session_service
if _session_service is None:
logger.info(f"Initializing DatabaseSessionService with {settings.DATABASE_URL}...")
_session_service = DatabaseSessionService(db_url=settings.DATABASE_URL)
logger.info("DatabaseSessionService initialized successfully")
async def close_session_service() -> None:
"""Cleanup session service resources on application shutdown."""
global _session_service
if _session_service:
_session_service = None
logger.info("DatabaseSessionService closed")
def get_session_service() -> DatabaseSessionService:
"""
Get the session service instance for dependency injection.
Raises:
RuntimeError: If service is not initialized.
"""
if _session_service is None:
raise RuntimeError("Session service not initialized. Application may not have started properly.")
return _session_service

View File

@@ -0,0 +1,146 @@
import os
import logging
import importlib.util
import inspect
from typing import List, Dict, Optional, Any, Callable
from pathlib import Path
# ADK imports
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
from google.adk.tools.mcp_tool.mcp_session_manager import (
StreamableHTTPServerParams,
StdioConnectionParams,
StdioServerParameters
)
from app.models import MCPServerConfig
logger = logging.getLogger(__name__)
class ToolService:
"""
Service for managing available tools (Local and MCP).
"""
def __init__(self, tools_dir: str = "tools"):
self.tools_dir = Path(tools_dir)
self._mcp_registry: Dict[str, MCPServerConfig] = {}
# Ensure tools directory exists
if not self.tools_dir.exists():
self.tools_dir.mkdir(parents=True, exist_ok=True)
# --- Local Tools Scanning ---
def get_local_tools(self) -> List[str]:
"""
Scan the 'tools/' directory for valid tool implementations.
Returns a list of tool names (folder names).
"""
tools = []
if not self.tools_dir.exists():
return []
for item in self.tools_dir.iterdir():
if item.is_dir():
# Check if it has a valid entry point
if (item / "tool.py").exists() or (item / "main.py").exists():
tools.append(item.name)
return tools
def load_local_tool(self, tool_name: str) -> Optional[Callable]:
"""
Dynamically load a local tool by name.
Expects a function named 'tool' or the directory name in tool.py/main.py.
"""
tool_path = self.tools_dir / tool_name
# Try finding the module
module_file = None
if (tool_path / "tool.py").exists():
module_file = tool_path / "tool.py"
elif (tool_path / "main.py").exists():
module_file = tool_path / "main.py"
if not module_file:
logger.warning(f"Tool implementation not found for {tool_name}")
return None
try:
spec = importlib.util.spec_from_file_location(f"tools.{tool_name}", module_file)
if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Look for a callable object named 'tool' or scan for functions
if hasattr(module, "tool") and callable(module.tool):
return module.tool
# Heuristic: Find first function that looks like a tool?
# For now, require 'tool' export or function with same name as folder
if hasattr(module, tool_name) and callable(getattr(module, tool_name)):
return getattr(module, tool_name)
logger.warning(f"No callable 'tool' or '{tool_name}' found in {module_file}")
except Exception as e:
logger.error(f"Failed to load tool {tool_name}: {e}")
return None
# --- MCP Registry ---
def list_mcp_servers(self) -> List[MCPServerConfig]:
"""Return all configured MCP servers."""
return list(self._mcp_registry.values())
def add_mcp_server(self, config: MCPServerConfig):
"""Register a new MCP server."""
self._mcp_registry[config.name] = config
logger.info(f"Registered MCP Server: {config.name}")
def remove_mcp_server(self, name: str) -> bool:
"""Remove an MCP server by name."""
if name in self._mcp_registry:
del self._mcp_registry[name]
logger.info(f"Removed MCP Server: {name}")
return True
return False
def get_mcp_toolset(self, name: str) -> Optional[MCPToolset]:
"""
Create an ADK MCPToolset instance from the configuration.
"""
config = self._mcp_registry.get(name)
if not config:
return None
try:
if config.type == "sse":
if not config.sse_config:
raise ValueError("Missing SSE config")
params = StreamableHTTPServerParams(url=config.sse_config.url)
return MCPToolset(connection_params=params)
elif config.type == "stdio":
if not config.stdio_config:
raise ValueError("Missing Stdio config")
server_params = StdioServerParameters(
command=config.stdio_config.command,
args=config.stdio_config.args,
env=config.stdio_config.env
)
params = StdioConnectionParams(server_params=server_params)
return MCPToolset(connection_params=params, tool_filter=config.tool_filter)
except Exception as e:
logger.error(f"Failed to create MCP Toolset '{name}': {e}")
return None
return None
# Singleton instance
_tool_service = ToolService()
def get_tool_service() -> ToolService:
return _tool_service