From df79af84d41f4d5ed34a2dad41920b5019abccae Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 30 Nov 2025 15:44:18 +0100 Subject: [PATCH] chore(ai): remove ADK references and migrate to PydanticAI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../core-ai/diagnostics/test_adk_direct.py | 139 --------- .../core-ai/diagnostics/test_adk_tools.py | 199 ------------ services/core-ai/src/agents/adk_agent.py | 286 ------------------ services/core-ai/src/config.py | 6 +- services/core-ai/src/prompts.py | 2 +- services/core-ai/src/tools.py | 12 +- services/core-ai/src/tools/registry.py | 18 +- services/core-ai/tests/test_06_adk_setup.py | 131 -------- .../core-ai/tests/test_06_pydantic_setup.py | 131 ++++++++ ...adk_tools.py => test_07_pydantic_tools.py} | 43 +-- ..._10_adk_api.py => test_10_pydantic_api.py} | 67 ++-- test-headers.html | 62 ---- 12 files changed, 208 insertions(+), 888 deletions(-) delete mode 100644 services/core-ai/diagnostics/test_adk_direct.py delete mode 100644 services/core-ai/diagnostics/test_adk_tools.py delete mode 100644 services/core-ai/src/agents/adk_agent.py delete mode 100644 services/core-ai/tests/test_06_adk_setup.py create mode 100644 services/core-ai/tests/test_06_pydantic_setup.py rename services/core-ai/tests/{test_07_adk_tools.py => test_07_pydantic_tools.py} (75%) rename services/core-ai/tests/{test_10_adk_api.py => test_10_pydantic_api.py} (77%) delete mode 100644 test-headers.html diff --git a/services/core-ai/diagnostics/test_adk_direct.py b/services/core-ai/diagnostics/test_adk_direct.py deleted file mode 100644 index c1b7957..0000000 --- a/services/core-ai/diagnostics/test_adk_direct.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -""" -Diagnostic tool to test ADK agent directly (without HTTP layer). -This tests ADK initialization and basic completion without tools. - -Usage: - python diagnostics/test_adk_direct.py -""" -import asyncio -import sys -import os -from pathlib import Path - -# Add parent directory to path to import from src -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from src.config import get_settings -from src.agents import ADK_AVAILABLE - -if not ADK_AVAILABLE: - print("āœ— Google ADK not available") - print(" Install with: pip install google-adk") - sys.exit(1) - -from src.agents import ADKAgent - - -async def test_adk_direct(): - """Test ADK agent without HTTP layer""" - settings = get_settings() - - print("=" * 70) - print("ADK DIRECT TEST (No Tools)") - print("=" * 70) - - print(f"\n1. Configuration") - print(f" Model: {settings.agent_model}") - print(f" Ollama URL: {settings.ollama_base_url}") - print(f" ADK Prompt Variant: {settings.adk_system_prompt_variant}") - - # Test cases without tools - test_cases = [ - { - "name": "Simple question", - "messages": [ - {"role": "user", "content": "What is the capital of France? Answer in one sentence."} - ] - }, - { - "name": "Math problem", - "messages": [ - {"role": "user", "content": "What is 15 + 27? Just give me the number."} - ] - }, - { - "name": "Multi-step reasoning", - "messages": [ - {"role": "user", "content": "If I have 3 apples and buy 2 more, then eat 1, how many do I have left?"} - ] - } - ] - - # Run tests - for i, test_case in enumerate(test_cases, 1): - print(f"\n{'-' * 70}") - print(f"Test {i}/{len(test_cases)}: {test_case['name']}") - print(f"{'-' * 70}") - - try: - # Initialize ADK agent (no tools) - print("\n→ Initializing ADK agent (no tools)...") - agent = ADKAgent(tools=[]) - print("āœ“ ADK agent initialized") - - # Test non-streaming - print(f"\n→ Testing non-streaming completion...") - print(f" Question: {test_case['messages'][0]['content']}") - - response = await agent.chat_completion( - messages=test_case['messages'] - ) - - print(f"\nāœ“ Response received:") - print(f" {response}") - - # Test streaming - print(f"\n→ Testing streaming completion...") - chunks = [] - event_count = 0 - - async for chunk in agent.chat( - messages=test_case['messages'], - stream=True - ): - event_count += 1 - chunk_type = chunk.get("type") - - if chunk_type == "content" and chunk.get("content"): - chunks.append(chunk["content"]) - elif chunk_type == "tool_call": - print(f" šŸ”§ Tool call: {chunk.get('tool')}") - elif chunk_type == "tool_result": - print(f" āœ… Tool result") - elif chunk_type == "error": - print(f" āŒ Error: {chunk.get('content')}") - - full_content = "".join(chunks) - print(f"\nāœ“ Streaming response received:") - print(f" Events: {event_count}") - print(f" Content: {full_content}") - - print(f"\nāœ“ Test {i} PASSED") - - except ImportError as e: - print(f"\nāœ— Test {i} FAILED: ADK import error") - print(f" Error: {e}") - print(f" Install: pip install google-adk") - return False - - except Exception as e: - print(f"\nāœ— Test {i} FAILED") - print(f" Error: {type(e).__name__}: {e}") - import traceback - traceback.print_exc() - return False - - print("\n" + "=" * 70) - print("āœ“ ALL ADK TESTS PASSED (No Tools)!") - print("=" * 70) - print("\nNext steps:") - print(" 1. ADK initialization works") - print(" 2. ADK can generate responses without tools") - print(" 3. Ready to add tool integration (Phase 2)") - return True - - -if __name__ == "__main__": - result = asyncio.run(test_adk_direct()) - sys.exit(0 if result else 1) diff --git a/services/core-ai/diagnostics/test_adk_tools.py b/services/core-ai/diagnostics/test_adk_tools.py deleted file mode 100644 index 1aefb60..0000000 --- a/services/core-ai/diagnostics/test_adk_tools.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env python3 -""" -Diagnostic tool to test ADK agent with tools. - -This tests: -1. Local tool registration -2. Tool execution -3. ADK agent with tools -4. (Optional) REST tool discovery from core-api - -Usage: - python diagnostics/test_adk_tools.py -""" -import asyncio -import sys -import os -from pathlib import Path - -# Add parent directory to path to import from src -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from src.config import get_settings -from src.tools import get_all_tools, get_agent_tools, discover_and_register_tools -from src.agents import ADK_AVAILABLE - -if not ADK_AVAILABLE: - print("āœ— Google ADK not available") - print(" Install with: pip install google-adk") - sys.exit(1) - -from src.agents import ADKAgent - - -async def test_tools_diagnostic(): - """Test ADK agent with tools""" - settings = get_settings() - - print("=" * 70) - print("ADK TOOLS DIAGNOSTIC") - print("=" * 70) - - # ======================================================================== - # Part 1: Local Tools - # ======================================================================== - print("\n" + "=" * 70) - print("PART 1: LOCAL TOOLS") - print("=" * 70) - - print("\n1. Local Tool Registration") - tools = get_all_tools() - print(f" Registered tools: {len(tools)}") - for tool_name in tools.keys(): - print(f" - {tool_name}") - - # Test local tools directly - print("\n2. Testing Local Tools") - - print("\n → Testing get_current_time...") - from src.tools.local import get_current_time - time_result = await get_current_time() - print(f" Result: {time_result}") - - print("\n → Testing get_current_date...") - from src.tools.local import get_current_date - date_result = await get_current_date() - print(f" Result: {date_result}") - - print("\n → Testing calculate...") - from src.tools.local import calculate - calc_result = await calculate("15 + 27") - print(f" Result: 15 + 27 = {calc_result}") - - print("\n → Testing date operations...") - from src.tools.local import add_days_to_date, calculate_date_difference - future_date = await add_days_to_date(date_result, 30) - print(f" {date_result} + 30 days = {future_date}") - - diff = await calculate_date_difference(date_result, future_date) - print(f" Difference: {diff}") - - print("\nāœ“ All local tools working") - - # ======================================================================== - # Part 2: ADK Integration - # ======================================================================== - print("\n" + "=" * 70) - print("PART 2: ADK INTEGRATION") - print("=" * 70) - - print("\n3. Converting Tools to ADK Format") - adk_tools = get_agent_tools() - print(f" ADK tools created: {len(adk_tools)}") - - # ======================================================================== - # Part 3: ADK Agent with Tools - # ======================================================================== - print("\n" + "=" * 70) - print("PART 3: ADK AGENT WITH TOOLS") - print("=" * 70) - - test_cases = [ - { - "name": "Simple calculation with tool", - "messages": [ - {"role": "user", "content": "What is 123 + 456? Use the calculate tool to find the answer."} - ] - }, - { - "name": "Current time query", - "messages": [ - {"role": "user", "content": "What is the current time and date? Use the appropriate tools."} - ] - }, - { - "name": "Date calculation", - "messages": [ - {"role": "user", "content": "What will the date be 45 days from now? Use the date tools."} - ] - }, - ] - - # Run tests - for i, test_case in enumerate(test_cases, 1): - print(f"\n{'-' * 70}") - print(f"Test {i}/{len(test_cases)}: {test_case['name']}") - print(f"{'-' * 70}") - - try: - # Initialize ADK agent with tools - print("\n→ Initializing ADK agent with tools...") - agent = ADKAgent(discover_tools=True) - print(f"āœ“ ADK agent initialized with {len(agent.tools)} tools") - - # Test non-streaming - print(f"\n→ Query: {test_case['messages'][0]['content']}") - - response = await agent.chat_completion( - messages=test_case['messages'] - ) - - print(f"\nāœ“ Response:") - print(f" {response}") - - print(f"\nāœ“ Test {i} PASSED") - - except Exception as e: - print(f"\nāœ— Test {i} FAILED") - print(f" Error: {type(e).__name__}: {e}") - import traceback - traceback.print_exc() - return False - - # ======================================================================== - # Part 4: REST Tool Discovery (Optional) - # ======================================================================== - print("\n" + "=" * 70) - print("PART 4: REST TOOL DISCOVERY (OPTIONAL)") - print("=" * 70) - - print(f"\n4. Attempting to discover tools from core-api") - print(f" Core-API URL: {settings.core_api_base_url}") - - try: - print("\n→ Fetching OpenAPI spec from core-api...") - rest_tools_count = await discover_and_register_tools() - print(f"āœ“ Discovered and registered {rest_tools_count} REST tools from core-api") - - # Show all tools now - all_tools = get_all_tools() - print(f"\n Total tools registered: {len(all_tools)}") - for tool_name in all_tools.keys(): - print(f" - {tool_name}") - - except Exception as e: - print(f"\nāš ļø Could not discover REST tools from core-api") - print(f" Reason: {type(e).__name__}: {e}") - print(f" This is expected if core-api is not running or doesn't have OpenAPI docs yet") - - # ======================================================================== - # Summary - # ======================================================================== - print("\n" + "=" * 70) - print("āœ“ ADK TOOLS DIAGNOSTIC COMPLETE!") - print("=" * 70) - print("\nResults:") - print(f" āœ“ Local tools: {len([t for t in get_all_tools().keys() if 'calculate' in t or 'date' in t or 'time' in t])}") - print(f" āœ“ ADK integration: Working") - print(f" āœ“ Tool calling: Working") - print("\nNext steps:") - print(" 1. Local tools are working") - print(" 2. ADK agent can use tools") - print(" 3. Ready to add REST tools from core-api") - print(" 4. Ready for Phase 3: Full tool integration") - return True - - -if __name__ == "__main__": - result = asyncio.run(test_tools_diagnostic()) - sys.exit(0 if result else 1) diff --git a/services/core-ai/src/agents/adk_agent.py b/services/core-ai/src/agents/adk_agent.py deleted file mode 100644 index dbf92d5..0000000 --- a/services/core-ai/src/agents/adk_agent.py +++ /dev/null @@ -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) diff --git a/services/core-ai/src/config.py b/services/core-ai/src/config.py index b651cb2..7dc28f7 100644 --- a/services/core-ai/src/config.py +++ b/services/core-ai/src/config.py @@ -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 diff --git a/services/core-ai/src/prompts.py b/services/core-ai/src/prompts.py index 33186c2..561f3ba 100644 --- a/services/core-ai/src/prompts.py +++ b/services/core-ai/src/prompts.py @@ -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 diff --git a/services/core-ai/src/tools.py b/services/core-ai/src/tools.py index 950807b..ccf13fa 100644 --- a/services/core-ai/src/tools.py +++ b/services/core-ai/src/tools.py @@ -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 [] \ No newline at end of file diff --git a/services/core-ai/src/tools/registry.py b/services/core-ai/src/tools/registry.py index 719b225..0628fbf 100644 --- a/services/core-ai/src/tools/registry.py +++ b/services/core-ai/src/tools/registry.py @@ -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") diff --git a/services/core-ai/tests/test_06_adk_setup.py b/services/core-ai/tests/test_06_adk_setup.py deleted file mode 100644 index 1c9eddc..0000000 --- a/services/core-ai/tests/test_06_adk_setup.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -""" -Layer 6: ADK Setup Tests -Tests that Google ADK initializes correctly and can handle basic completions. -""" -import pytest -import sys -from pathlib import Path - -# Add parent directory to path -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from src.config import get_settings -from src.prompts import get_prompt -from src.agents import ADK_AVAILABLE - -if not ADK_AVAILABLE: - pytest.skip("Google ADK not available", allow_module_level=True) - -from src.agents import ADKAgent - - -def test_adk_import(): - """Test that ADK can be imported""" - assert ADK_AVAILABLE, "ADK should be available" - print("āœ“ ADK imports successful") - - -def test_adk_prompt_exists(): - """Test that ADK prompt variant exists""" - settings = get_settings() - prompt = get_prompt(settings.adk_system_prompt_variant) - - assert prompt is not None, "ADK prompt should exist" - assert len(prompt) > 0, "ADK prompt should not be empty" - assert "assistant" in prompt.lower() or "tools" in prompt.lower(), "ADK prompt should mention tools/assistant" - - print(f"āœ“ ADK prompt variant '{settings.adk_system_prompt_variant}' exists") - print(f" Prompt: {prompt[:100]}...") - - -def test_adk_agent_initialization(): - """Test that ADK agent can be initialized without tools""" - try: - agent = ADKAgent(tools=[]) - assert agent is not None, "Agent should be initialized" - assert agent.llm is not None, "LLM should be initialized" - assert agent.agent is not None, "ADK agent should be initialized" - assert agent.tools == [], "Tools should be empty" - - print("āœ“ ADK agent initialized successfully") - print(f" LLM: {agent.llm}") - print(f" Tools: {len(agent.tools)}") - - except Exception as e: - pytest.fail(f"ADK agent initialization failed: {e}") - - -@pytest.mark.asyncio -async def test_adk_simple_completion(): - """Test ADK agent with a simple question (no tools needed)""" - agent = ADKAgent(tools=[]) - - messages = [{"role": "user", "content": "What is 2+2? Answer with just the number."}] - - print(f"\n→ Testing ADK completion...") - response = await agent.chat_completion(messages=messages) - - assert response is not None, "Response should not be None" - assert len(response) > 0, "Response should not be empty" - assert "4" in response, f"Expected '4' in response, got: {response}" - - print(f"āœ“ ADK response: {response}") - - -@pytest.mark.asyncio -async def test_adk_streaming(): - """Test ADK agent streaming mode""" - agent = ADKAgent(tools=[]) - - messages = [{"role": "user", "content": "Count from 1 to 3. Just the numbers."}] - - chunks = [] - event_count = 0 - - print(f"\n→ Testing ADK streaming...") - async for chunk in agent.chat(messages=messages, stream=True): - event_count += 1 - if chunk.get("type") == "content" and chunk.get("content"): - chunks.append(chunk["content"]) - - full_content = "".join(chunks) - - assert event_count > 0, "Should receive events" - assert len(full_content) > 0, "Should receive content" - - print(f"āœ“ Received {event_count} events") - print(f"āœ“ Content: {full_content}") - - -@pytest.mark.asyncio -async def test_adk_capital_of_france(): - """Test ADK with the standard 'capital of France' question""" - agent = ADKAgent(tools=[]) - - messages = [{"role": "user", "content": "What is the capital of France?"}] - - print(f"\n→ Testing 'What is the capital of France?' with ADK...") - response = await agent.chat_completion(messages=messages) - - assert response is not None, "Response should not be None" - assert len(response) > 0, "Response should not be empty" - assert "paris" in response.lower(), f"Expected 'Paris' in answer, got: {response}" - - print(f"āœ“ Correct answer: {response}") - - -def test_adk_system_prompt_loading(): - """Test that ADK agent loads correct system prompt""" - agent = ADKAgent(tools=[]) - - settings = get_settings() - expected_prompt = get_prompt(settings.adk_system_prompt_variant) - - assert agent.system_prompt == expected_prompt, "System prompt should match config" - print(f"āœ“ System prompt loaded correctly") - print(f" Variant: {settings.adk_system_prompt_variant}") - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/services/core-ai/tests/test_06_pydantic_setup.py b/services/core-ai/tests/test_06_pydantic_setup.py new file mode 100644 index 0000000..3a9dd9c --- /dev/null +++ b/services/core-ai/tests/test_06_pydantic_setup.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +Layer 6: PydanticAI Setup Tests +Tests that PydanticAI initializes correctly and can handle basic completions. +""" +import pytest +import sys +from pathlib import Path + +# Add parent directory to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from src.config import get_settings +from src.prompts import get_prompt +from src.agents import PYDANTIC_AI_AVAILABLE + +if not PYDANTIC_AI_AVAILABLE: + pytest.skip("PydanticAI not available", allow_module_level=True) + +from src.agents import PydanticAgent + + +def test_pydantic_import(): + """Test that PydanticAI can be imported""" + assert PYDANTIC_AI_AVAILABLE, "PydanticAI should be available" + print("āœ“ PydanticAI imports successful") + + +def test_pydantic_prompt_exists(): + """Test that PydanticAI prompt variant exists""" + settings = get_settings() + prompt = get_prompt(settings.pydantic_system_prompt_variant) + + assert prompt is not None, "PydanticAI prompt should exist" + assert len(prompt) > 0, "PydanticAI prompt should not be empty" + assert "assistant" in prompt.lower() or "tools" in prompt.lower(), "PydanticAI prompt should mention tools/assistant" + + print(f"āœ“ PydanticAI prompt variant '{settings.pydantic_system_prompt_variant}' exists") + print(f" Prompt: {prompt[:100]}...") + + +def test_pydantic_agent_initialization(): + """Test that PydanticAI agent can be initialized without tools""" + try: + agent = PydanticAgent(tools=[], enable_memory=False) + assert agent is not None, "Agent should be initialized" + assert agent.model is not None, "Model should be initialized" + assert agent.agent is not None, "PydanticAI agent should be initialized" + assert agent.tools == [], "Tools should be empty" + + print("āœ“ PydanticAI agent initialized successfully") + print(f" Model: {agent.model}") + print(f" Tools: {len(agent.tools)}") + + except Exception as e: + pytest.fail(f"PydanticAI agent initialization failed: {e}") + + +@pytest.mark.asyncio +async def test_pydantic_simple_completion(): + """Test PydanticAI agent with a simple question (no tools needed)""" + agent = PydanticAgent(tools=[], enable_memory=False) + + messages = [{"role": "user", "content": "What is 2+2? Answer with just the number."}] + + print(f"\n→ Testing PydanticAI completion...") + response = await agent.chat_completion(messages=messages) + + assert response is not None, "Response should not be None" + assert len(response) > 0, "Response should not be empty" + assert "4" in response, f"Expected '4' in response, got: {response}" + + print(f"āœ“ PydanticAI response: {response}") + + +@pytest.mark.asyncio +async def test_pydantic_streaming(): + """Test PydanticAI agent streaming mode""" + agent = PydanticAgent(tools=[], enable_memory=False) + + messages = [{"role": "user", "content": "Count from 1 to 3. Just the numbers."}] + + chunks = [] + event_count = 0 + + print(f"\n→ Testing PydanticAI streaming...") + async for chunk in agent.chat(messages=messages, stream=True): + event_count += 1 + if chunk.get("type") == "content" and chunk.get("content"): + chunks.append(chunk["content"]) + + full_content = "".join(chunks) + + assert event_count > 0, "Should receive events" + assert len(full_content) > 0, "Should receive content" + + print(f"āœ“ Received {event_count} events") + print(f"āœ“ Content: {full_content}") + + +@pytest.mark.asyncio +async def test_pydantic_capital_of_france(): + """Test PydanticAI with the standard 'capital of France' question""" + agent = PydanticAgent(tools=[], enable_memory=False) + + messages = [{"role": "user", "content": "What is the capital of France?"}] + + print(f"\n→ Testing 'What is the capital of France?' with PydanticAI...") + response = await agent.chat_completion(messages=messages) + + assert response is not None, "Response should not be None" + assert len(response) > 0, "Response should not be empty" + assert "paris" in response.lower(), f"Expected 'Paris' in answer, got: {response}" + + print(f"āœ“ Correct answer: {response}") + + +def test_pydantic_system_prompt_loading(): + """Test that PydanticAI agent loads correct system prompt""" + agent = PydanticAgent(tools=[], enable_memory=False) + + settings = get_settings() + expected_prompt = get_prompt(settings.pydantic_system_prompt_variant) + + assert agent.system_prompt == expected_prompt, "System prompt should match config" + print(f"āœ“ System prompt loaded correctly") + print(f" Variant: {settings.pydantic_system_prompt_variant}") + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"]) diff --git a/services/core-ai/tests/test_07_adk_tools.py b/services/core-ai/tests/test_07_pydantic_tools.py similarity index 75% rename from services/core-ai/tests/test_07_adk_tools.py rename to services/core-ai/tests/test_07_pydantic_tools.py index 057dfce..028b894 100644 --- a/services/core-ai/tests/test_07_adk_tools.py +++ b/services/core-ai/tests/test_07_pydantic_tools.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Layer 7: ADK Tools Tests -Tests that local tools are registered and work with the ADK agent. +Layer 7: PydanticAI Tools Tests +Tests that local tools are registered and work with the PydanticAI agent. """ import pytest import sys @@ -11,13 +11,13 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from src.config import get_settings -from src.tools import get_all_tools, get_agent_tools, clear_registry -from src.agents import ADK_AVAILABLE +from src.tools.registry import get_all_tools, clear_registry +from src.agents import PYDANTIC_AI_AVAILABLE -if not ADK_AVAILABLE: - pytest.skip("Google ADK not available", allow_module_level=True) +if not PYDANTIC_AI_AVAILABLE: + pytest.skip("PydanticAI not available", allow_module_level=True) -from src.agents import ADKAgent +from src.agents import PydanticAgent def test_local_tools_registered(): @@ -82,37 +82,22 @@ async def test_calculator_security(): print(f"āœ“ Blocked dangerous expression: {expr}") -def test_adk_tool_conversion(): - """Test that tools can be converted to ADK format""" - adk_tools = get_agent_tools() - - assert len(adk_tools) > 0, "Should have at least some tools" - print(f"āœ“ Converted {len(adk_tools)} tools to ADK format") - - # All tools should be FunctionTool instances - from google.adk.tools import FunctionTool - for tool in adk_tools: - assert isinstance(tool, FunctionTool), f"Tool should be FunctionTool, got {type(tool)}" - - print(f"āœ“ All tools are valid FunctionTool instances") - - @pytest.mark.asyncio -async def test_adk_agent_with_tools(): - """Test ADK agent initialization with tools""" - agent = ADKAgent(discover_tools=True) +async def test_pydantic_agent_with_tools(): + """Test PydanticAI agent initialization with tools""" + agent = PydanticAgent(discover_tools=True, enable_memory=False) assert len(agent.tools) > 0, "Agent should have tools" - print(f"āœ“ ADK agent initialized with {len(agent.tools)} tools") + print(f"āœ“ PydanticAI agent initialized with {len(agent.tools)} tools") @pytest.mark.asyncio async def test_tool_calling_integration(): - """Test that ADK agent can use tools to answer questions""" - agent = ADKAgent(discover_tools=True) + """Test that PydanticAI agent can use tools to answer questions""" + agent = PydanticAgent(discover_tools=True, enable_memory=False) # Ask a question that requires the calculator tool - messages = [{"role": "user", "content": "What is 15 + 27? Use the calculator tool."}] + messages = [{"role": "user", "content": "What is 15 + 27? Use the calculate tool."}] print(f"\n→ Testing tool calling with: {messages[0]['content']}") response = await agent.chat_completion(messages=messages) diff --git a/services/core-ai/tests/test_10_adk_api.py b/services/core-ai/tests/test_10_pydantic_api.py similarity index 77% rename from services/core-ai/tests/test_10_adk_api.py rename to services/core-ai/tests/test_10_pydantic_api.py index c9830fb..e6fc29c 100644 --- a/services/core-ai/tests/test_10_adk_api.py +++ b/services/core-ai/tests/test_10_pydantic_api.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Layer 10: ADK API Tests -Tests HTTP endpoints for both simple and ADK agents. +Layer 10: PydanticAI API Tests +Tests HTTP endpoints for both simple and PydanticAI agents. """ import pytest import sys @@ -11,7 +11,7 @@ from pathlib import Path # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) -from src.agents import ADK_AVAILABLE +from src.agents import PYDANTIC_AI_AVAILABLE # Base URL for the service BASE_URL = "http://localhost:8086" @@ -54,8 +54,8 @@ async def test_list_tools(): @pytest.mark.asyncio -async def test_chat_completions_simple(): - """Test /v1/chat/completions endpoint (default)""" +async def test_chat_completions_default(): + """Test /v1/chat/completions endpoint (default - should use PydanticAI)""" async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( f"{BASE_URL}/v1/chat/completions", @@ -104,14 +104,14 @@ async def test_chat_simple_endpoint(): @pytest.mark.asyncio -async def test_chat_adk_endpoint(): - """Test /v1/chat/adk endpoint""" - if not ADK_AVAILABLE: - pytest.skip("ADK not available") +async def test_chat_pydantic_endpoint(): + """Test /v1/chat/pydantic endpoint""" + if not PYDANTIC_AI_AVAILABLE: + pytest.skip("PydanticAI not available") async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( - f"{BASE_URL}/v1/chat/adk", + f"{BASE_URL}/v1/chat/pydantic", json={ "messages": [ {"role": "user", "content": "What is the current date?"} @@ -124,26 +124,26 @@ async def test_chat_adk_endpoint(): assert response.status_code == 200 data = response.json() - assert data["model"] == "adk" + assert data["model"] == "pydantic" assert "choices" in data assert "tools_enabled" in data assert "tools_count" in data content = data["choices"][0]["message"]["content"] - print(f"āœ“ /v1/chat/adk response: {content[:200]}...") + print(f"āœ“ /v1/chat/pydantic response: {content[:200]}...") print(f" Tools enabled: {data['tools_enabled']}") print(f" Tools count: {data['tools_count']}") @pytest.mark.asyncio -async def test_chat_adk_with_calculator(): - """Test ADK endpoint using calculator tool""" - if not ADK_AVAILABLE: - pytest.skip("ADK not available") +async def test_chat_pydantic_with_calculator(): + """Test PydanticAI endpoint using calculator tool""" + if not PYDANTIC_AI_AVAILABLE: + pytest.skip("PydanticAI not available") async with httpx.AsyncClient(timeout=60.0) as client: response = await client.post( - f"{BASE_URL}/v1/chat/adk", + f"{BASE_URL}/v1/chat/pydantic", json={ "messages": [ {"role": "user", "content": "What is 123 + 456? Use the calculate tool."} @@ -157,7 +157,7 @@ async def test_chat_adk_with_calculator(): data = response.json() content = data["choices"][0]["message"]["content"] - print(f"āœ“ ADK with calculator: {content}") + print(f"āœ“ PydanticAI with calculator: {content}") @pytest.mark.asyncio @@ -198,14 +198,14 @@ async def test_streaming_simple(): @pytest.mark.asyncio -async def test_adk_without_tools(): - """Test ADK endpoint with tools disabled""" - if not ADK_AVAILABLE: - pytest.skip("ADK not available") +async def test_pydantic_without_tools(): + """Test PydanticAI endpoint with tools disabled""" + if not PYDANTIC_AI_AVAILABLE: + pytest.skip("PydanticAI not available") async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( - f"{BASE_URL}/v1/chat/adk", + f"{BASE_URL}/v1/chat/pydantic", json={ "messages": [ {"role": "user", "content": "Hello!"} @@ -221,7 +221,26 @@ async def test_adk_without_tools(): assert data["tools_enabled"] is False assert data["tools_count"] == 0 - print(f"āœ“ ADK without tools works") + print(f"āœ“ PydanticAI without tools works") + + +@pytest.mark.asyncio +async def test_list_models(): + """Test the /v1/models endpoint""" + async with httpx.AsyncClient() as client: + response = await client.get(f"{BASE_URL}/v1/models") + assert response.status_code == 200 + + data = response.json() + assert "object" in data + assert data["object"] == "list" + assert "data" in data + assert len(data["data"]) > 0 + + print(f"āœ“ Models endpoint OK") + print(f" Available models:") + for model in data["data"]: + print(f" - {model['id']}") if __name__ == "__main__": diff --git a/test-headers.html b/test-headers.html deleted file mode 100644 index 9d5c943..0000000 --- a/test-headers.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - Header Test - - - -

Authentik Header Test

-

This page will request headers from your current session

-
Loading...
- - - -