chore(ai): remove ADK references and migrate to PydanticAI
This commit completes the cleanup of Google ADK references after migrating to PydanticAI. Changes: - Removed ADK agent implementation (adk_agent.py) - Removed ADK test files (test_06, test_07, test_10) - Removed ADK diagnostic files - Updated config to use pydantic_system_prompt_variant instead of adk_system_prompt_variant - Updated prompts.py to rename adk_agent to pydantic_agent - Updated tool registry and tools.py docstrings to remove ADK references - Added new comprehensive PydanticAI tests (test_06, test_07, test_10) - Marked legacy ADK functions as deprecated for backwards compatibility The codebase is now clean and stable with PydanticAI as the primary agent framework. Docker container builds successfully with no ADK import errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,286 +0,0 @@
|
||||
"""
|
||||
ADK Agent - Google ADK with LiteLLM backend and tool calling support.
|
||||
|
||||
Based on official documentation:
|
||||
- https://google.github.io/adk-docs/get-started/python/
|
||||
- https://docs.litellm.ai/docs/tutorials/google_adk
|
||||
- https://medium.com/@viplav.fauzdar/building-a-local-ai-agent-with-google-adk-litellm-and-ollama-6e907e2db268
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from typing import AsyncIterator, Dict, Any, List, Optional
|
||||
from functools import lru_cache
|
||||
|
||||
# Google ADK imports (official API)
|
||||
try:
|
||||
from google.adk.agents import Agent
|
||||
from google.adk.models.lite_llm import LiteLlm
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
from google.adk.runners import Runner
|
||||
from google.genai import types
|
||||
ADK_AVAILABLE = True
|
||||
except ImportError:
|
||||
ADK_AVAILABLE = False
|
||||
Agent = None
|
||||
LiteLlm = None
|
||||
InMemorySessionService = None
|
||||
Runner = None
|
||||
types = None
|
||||
|
||||
from src.config import get_settings
|
||||
from src.prompts import get_prompt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ADKAgent:
|
||||
"""
|
||||
Agent using Google ADK with LiteLLM backend for Ollama.
|
||||
Supports tool calling and complex orchestration.
|
||||
|
||||
Example:
|
||||
agent = ADKAgent(tools=[my_tool])
|
||||
response = await agent.chat_completion(messages=[{"role": "user", "content": "Hello"}])
|
||||
"""
|
||||
|
||||
def __init__(self, tools: List = None, discover_tools: bool = False):
|
||||
if not ADK_AVAILABLE:
|
||||
raise ImportError("Google ADK not available. Install with: pip install google-adk")
|
||||
|
||||
logger.info("ADKAgent: Initializing Google ADK agent...")
|
||||
|
||||
self.settings = get_settings()
|
||||
|
||||
# Tools can be provided explicitly or discovered
|
||||
if tools is not None:
|
||||
# Explicit tools provided
|
||||
self.tools = tools
|
||||
logger.info(f"ADKAgent: Using {len(tools)} explicitly provided tools")
|
||||
elif discover_tools:
|
||||
# Discover tools from registry (includes local + core-api)
|
||||
logger.info("ADKAgent: Discovering tools from registry...")
|
||||
from src.tools import get_agent_tools
|
||||
self.tools = get_agent_tools()
|
||||
logger.info(f"ADKAgent: Discovered {len(self.tools)} tools")
|
||||
else:
|
||||
# No tools
|
||||
self.tools = []
|
||||
logger.info("ADKAgent: No tools enabled")
|
||||
|
||||
# Load system prompt for ADK mode
|
||||
adk_prompt_variant = getattr(self.settings, 'adk_system_prompt_variant', 'adk_agent')
|
||||
self.system_prompt = get_prompt(adk_prompt_variant)
|
||||
logger.info(f"ADKAgent: System prompt variant: {adk_prompt_variant}")
|
||||
logger.info(f"ADKAgent: System prompt: {self.system_prompt[:100]}...")
|
||||
|
||||
# Initialize LiteLlm for Ollama
|
||||
# Note: ollama_chat/ doesn't execute tools, so using ollama/ for tool calling
|
||||
# Testing with mistral-nemo which has better tool support than gemma2
|
||||
model_name = self.settings.agent_model
|
||||
litellm_model = f"ollama/{model_name}"
|
||||
|
||||
logger.info(f"ADKAgent: Initializing LiteLlm model: {litellm_model}")
|
||||
logger.info(f"ADKAgent: Ollama API base: {self.settings.ollama_base_url}")
|
||||
logger.info(f"ADKAgent: Tools registered: {len(self.tools)}")
|
||||
|
||||
# Create LiteLlm model instance
|
||||
self.model = LiteLlm(
|
||||
model=litellm_model,
|
||||
api_base=self.settings.ollama_base_url,
|
||||
stream=True,
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
# Create ADK Agent with the model
|
||||
self.agent = Agent(
|
||||
name="core_ai_agent",
|
||||
model=self.model,
|
||||
description="AI assistant for system management and Q&A",
|
||||
instruction=self.system_prompt,
|
||||
tools=self.tools,
|
||||
)
|
||||
|
||||
# Create session service and runner
|
||||
self.session_service = InMemorySessionService()
|
||||
self.runner = Runner(
|
||||
agent=self.agent,
|
||||
app_name="core-ai",
|
||||
session_service=self.session_service
|
||||
)
|
||||
|
||||
logger.info("✓ ADKAgent: Initialization complete")
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
conversation_id: str = None,
|
||||
stream: bool = True,
|
||||
prompt_variant: Optional[str] = None
|
||||
) -> AsyncIterator[Dict[str, Any]]:
|
||||
"""
|
||||
Process a chat message using ADK agent.
|
||||
|
||||
Args:
|
||||
messages: List of message dicts with 'role' and 'content'
|
||||
conversation_id: Optional conversation ID for session tracking
|
||||
stream: Whether to stream responses
|
||||
prompt_variant: Optional prompt variant (not used, set in __init__)
|
||||
|
||||
Yields:
|
||||
Dict with 'type' and content. Types:
|
||||
- {"type": "content", "content": "text chunk"}
|
||||
- {"type": "content", "content": "", "finish_reason": "stop"}
|
||||
- {"type": "error", "content": "error message"}
|
||||
"""
|
||||
logger.info(f"🚀 ADKAgent: Starting completion for message: {messages[-1]['content'][:50]}...")
|
||||
|
||||
try:
|
||||
# Extract user message (ADK handles system prompt internally)
|
||||
user_messages = [m for m in messages if m["role"] != "system"]
|
||||
if not user_messages:
|
||||
raise ValueError("No user messages provided")
|
||||
|
||||
# Use the last user message
|
||||
user_query = user_messages[-1]["content"]
|
||||
logger.info(f"📤 ADKAgent: User query: {user_query[:100]}...")
|
||||
|
||||
# Create unique user and session IDs
|
||||
user_id = "core-ai-user"
|
||||
session_id = conversation_id or str(uuid.uuid4())
|
||||
|
||||
# Always create a new session for each request (simple approach)
|
||||
# TODO: Implement session reuse for conversation continuity
|
||||
try:
|
||||
await self.session_service.create_session(
|
||||
app_name="core-ai",
|
||||
user_id=user_id,
|
||||
session_id=session_id
|
||||
)
|
||||
logger.info(f"✓ Created session: {session_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Session creation warning: {e} - attempting to use existing session")
|
||||
|
||||
# Create content for ADK
|
||||
content = types.Content(
|
||||
role='user',
|
||||
parts=[types.Part(text=user_query)]
|
||||
)
|
||||
|
||||
# Run agent and collect events
|
||||
final_response_text = ""
|
||||
event_count = 0
|
||||
|
||||
async for event in self.runner.run_async(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
new_message=content
|
||||
):
|
||||
event_count += 1
|
||||
|
||||
# Check for tool calls (official ADK method)
|
||||
calls = event.get_function_calls()
|
||||
if calls:
|
||||
for call in calls:
|
||||
tool_name = call.name if hasattr(call, 'name') else 'unknown'
|
||||
logger.info(f"🔧 Tool call: {tool_name}")
|
||||
continue
|
||||
|
||||
# Check for tool responses (official ADK method)
|
||||
responses = event.get_function_responses()
|
||||
if responses:
|
||||
for response in responses:
|
||||
# FunctionResponse has 'response' dict, not 'content'
|
||||
result = getattr(response, 'response', {})
|
||||
logger.info(f"✅ Tool response: {result}")
|
||||
continue
|
||||
|
||||
# Check for intermediate content (thinking/reasoning)
|
||||
if event.content and event.content.parts and not event.is_final_response():
|
||||
part = event.content.parts[0]
|
||||
intermediate_text = getattr(part, 'text', None)
|
||||
if intermediate_text:
|
||||
logger.debug(f"💭 Intermediate: {intermediate_text[:100]}...")
|
||||
continue
|
||||
|
||||
# Check if this is the final response
|
||||
if event.is_final_response():
|
||||
if event.content and event.content.parts:
|
||||
final_response_text = event.content.parts[0].text
|
||||
logger.info(f"📥 ADKAgent: Final response after {event_count} events")
|
||||
|
||||
# Yield content
|
||||
if stream:
|
||||
# Simulate streaming by yielding in chunks
|
||||
chunk_size = 50
|
||||
for i in range(0, len(final_response_text), chunk_size):
|
||||
chunk = final_response_text[i:i+chunk_size]
|
||||
yield {"type": "content", "content": chunk}
|
||||
|
||||
# Final chunk with finish reason
|
||||
yield {"type": "content", "content": "", "finish_reason": "stop"}
|
||||
else:
|
||||
# Non-streaming: yield full response
|
||||
yield {"type": "content", "content": final_response_text, "finish_reason": "stop"}
|
||||
# Don't break - let loop complete for callbacks (official recommendation)
|
||||
|
||||
# If no final response was received
|
||||
if not final_response_text:
|
||||
logger.warning(f"ADKAgent: No final response after {event_count} events")
|
||||
yield {
|
||||
"type": "error",
|
||||
"content": "Agent did not produce a final response.",
|
||||
"finish_reason": "error"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ADKAgent: Error during chat: {e}", exc_info=True)
|
||||
yield {
|
||||
"type": "error",
|
||||
"content": f"Sorry, an error occurred: {str(e)}",
|
||||
"finish_reason": "error"
|
||||
}
|
||||
|
||||
async def chat_completion(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
conversation_id: str = None,
|
||||
prompt_variant: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Get a non-streaming response from the ADK agent.
|
||||
|
||||
Args:
|
||||
messages: List of message dicts
|
||||
conversation_id: Optional conversation ID
|
||||
prompt_variant: Optional prompt variant
|
||||
|
||||
Returns:
|
||||
Complete response string
|
||||
"""
|
||||
final_content = ""
|
||||
async for chunk in self.chat(messages=messages, conversation_id=conversation_id, stream=False, prompt_variant=prompt_variant):
|
||||
if chunk["type"] == "content":
|
||||
final_content += chunk["content"]
|
||||
if chunk.get("finish_reason"):
|
||||
break
|
||||
|
||||
return final_content if final_content else "I couldn't generate a response."
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_adk_agent(tools: tuple = None, discover_tools: bool = False) -> ADKAgent:
|
||||
"""
|
||||
Get cached ADK agent instance.
|
||||
|
||||
Note: tools must be a tuple for caching to work.
|
||||
Convert list to tuple before calling: get_adk_agent(tuple(tools))
|
||||
|
||||
Args:
|
||||
tools: Tuple of tool functions (None to use discovery)
|
||||
discover_tools: Whether to discover tools from registry
|
||||
|
||||
Returns:
|
||||
Cached ADKAgent instance
|
||||
"""
|
||||
tools_list = list(tools) if tools is not None else None
|
||||
return ADKAgent(tools=tools_list, discover_tools=discover_tools)
|
||||
@@ -25,18 +25,18 @@ class Settings(BaseSettings):
|
||||
ollama_timeout: int = 300 # 5 minutes
|
||||
|
||||
# Model Configuration
|
||||
agent_model: str = "gemma2:9b-instruct-q5_K_M" # Optimized for ADK tool calling
|
||||
agent_model: str = "gemma2:9b-instruct-q5_K_M" # Optimized for PydanticAI tool calling
|
||||
|
||||
# System Prompt Variants
|
||||
system_prompt_variant: str = "minimal_agent" # For simple mode
|
||||
adk_system_prompt_variant: str = "adk_agent" # For ADK mode
|
||||
pydantic_system_prompt_variant: str = "pydantic_agent" # For PydanticAI mode
|
||||
|
||||
# Base URL for Core API tools (e.g., system status, services)
|
||||
core_api_base_url: str = "http://core-api:8083/v1"
|
||||
|
||||
# Feature Flags
|
||||
simple_enabled: bool = True # Enable simple endpoint
|
||||
adk_enabled: bool = True # Enable ADK endpoint
|
||||
pydantic_enabled: bool = True # Enable PydanticAI endpoint
|
||||
|
||||
# Memory System Configuration
|
||||
memory_enabled: bool = True
|
||||
|
||||
@@ -7,7 +7,7 @@ This file contains minimal, clean prompts for the Core AI service.
|
||||
PROMPTS = {
|
||||
"minimal_agent": """You are a helpful assistant. You can answer questions. If you need information, use the available tools.""",
|
||||
|
||||
"adk_agent": """You are a system management assistant with access to powerful tools.
|
||||
"pydantic_agent": """You are a system management assistant with access to powerful tools.
|
||||
|
||||
Your capabilities:
|
||||
- System status monitoring
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Agent Tools - Google ADK-compatible tools for the Core AI agent
|
||||
Agent Tools - Tools for the Core AI agent
|
||||
|
||||
These tools make REST API calls to the Core API service.
|
||||
"""
|
||||
@@ -83,9 +83,9 @@ async def response(answer: str) -> None:
|
||||
logger.info("`response` tool called. Returning None to terminate agent loop.")
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
# Tool Registry - ADK Format
|
||||
# ============================================================================
|
||||
# ============================================================================
|
||||
# Tool Registry - Legacy (deprecated, use src/tools/registry.py instead)
|
||||
# ============================================================================
|
||||
|
||||
try:
|
||||
from google.adk.tools import FunctionTool
|
||||
@@ -95,7 +95,7 @@ except ImportError:
|
||||
FunctionTool = None
|
||||
|
||||
|
||||
def get_agent_tools() -> List[FunctionTool]:
|
||||
"""Get all tools available to the agent for the current test phase"""
|
||||
def get_agent_tools() -> List:
|
||||
"""DEPRECATED: Get all tools available to the agent. Use src/tools/registry.py instead."""
|
||||
logger.info("--- DIAGNOSTIC MODE (Phase 1): Agent has NO tools. ---")
|
||||
return []
|
||||
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
Tool Registry - Manages tool registration and discovery for ADK agent.
|
||||
Tool Registry - Manages tool registration and discovery for AI agents.
|
||||
|
||||
This module provides a central registry for ADK-compatible tools.
|
||||
Tools can be registered, discovered, and provided to the ADK agent.
|
||||
This module provides a central registry for tools.
|
||||
Tools can be registered, discovered, and provided to AI agents.
|
||||
"""
|
||||
import logging
|
||||
import functools
|
||||
@@ -23,7 +23,7 @@ http_client = httpx.AsyncClient()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Google ADK Integration
|
||||
# Legacy ADK Integration (deprecated - kept for backwards compatibility)
|
||||
# ============================================================================
|
||||
|
||||
try:
|
||||
@@ -32,7 +32,6 @@ try:
|
||||
except ImportError:
|
||||
ADK_AVAILABLE = False
|
||||
FunctionTool = None
|
||||
logger.warning("Google ADK not available - tools will not be registered")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -74,7 +73,7 @@ def log_tool_call(func):
|
||||
|
||||
def register_tool(func: Callable) -> Callable:
|
||||
"""
|
||||
Register a tool function for use with the ADK agent.
|
||||
Register a tool function for use with AI agents.
|
||||
|
||||
Usage:
|
||||
@register_tool
|
||||
@@ -105,10 +104,13 @@ def get_all_tools() -> Dict[str, Callable]:
|
||||
|
||||
def get_agent_tools() -> List:
|
||||
"""
|
||||
Get all tools as ADK FunctionTool objects.
|
||||
DEPRECATED: Get all tools as ADK FunctionTool objects.
|
||||
|
||||
This function is kept for backwards compatibility but is no longer used.
|
||||
Use get_all_tools() instead for PydanticAI agents.
|
||||
|
||||
Returns:
|
||||
List of FunctionTool objects for ADK agent
|
||||
List of FunctionTool objects for legacy ADK agent
|
||||
"""
|
||||
if not ADK_AVAILABLE:
|
||||
logger.warning("ADK not available - returning empty tool list")
|
||||
|
||||
Reference in New Issue
Block a user