diff --git a/create_sample_exams.py b/create_sample_exams.py index 089d8b9..5bd87ed 100755 --- a/create_sample_exams.py +++ b/create_sample_exams.py @@ -6,12 +6,16 @@ in the input/ folder, so you can test the system without needing real exam papers. """ -from reportlab.lib.pagesizes import letter -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from reportlab.lib.units import inch -from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak -from reportlab.lib.enums import TA_CENTER, TA_LEFT -import os +try: + from reportlab.lib.pagesizes import letter + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + from reportlab.lib.units import inch + from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer + from reportlab.lib.enums import TA_CENTER + REPORTLAB_AVAILABLE = True +except ImportError: + REPORTLAB_AVAILABLE = False + from pathlib import Path # Import path utilities @@ -72,7 +76,7 @@ def create_sample_pdf(filename: str, exam_data: dict): """Create a sample exam PDF with the given questions.""" filepath = str(get_input_path(filename)) # Convert Path to string - + # Create PDF document doc = SimpleDocTemplate( filepath, @@ -82,10 +86,10 @@ def create_sample_pdf(filename: str, exam_data: dict): topMargin=72, bottomMargin=18 ) - + # Container for the 'Flowable' objects elements = [] - + # Define styles styles = getSampleStyleSheet() title_style = ParagraphStyle( @@ -96,7 +100,7 @@ def create_sample_pdf(filename: str, exam_data: dict): spaceAfter=30, alignment=TA_CENTER ) - + question_style = ParagraphStyle( 'Question', parent=styles['BodyText'], @@ -104,12 +108,12 @@ def create_sample_pdf(filename: str, exam_data: dict): spaceAfter=12, leftIndent=20 ) - + # Add title title = Paragraph(exam_data["title"], title_style) elements.append(title) elements.append(Spacer(1, 0.2*inch)) - + # Add instructions instructions = Paragraph( "Instructions: Answer all questions. Show all work for calculation problems. " @@ -118,51 +122,51 @@ def create_sample_pdf(filename: str, exam_data: dict): ) elements.append(instructions) elements.append(Spacer(1, 0.3*inch)) - + # Add questions for question_text, bloom_level in exam_data["questions"]: question = Paragraph(question_text, question_style) elements.append(question) elements.append(Spacer(1, 0.15*inch)) - + # Add footer elements.append(Spacer(1, 0.5*inch)) footer = Paragraph( - f"This is a sample exam generated for testing purposes. " - f"Bloom's levels included for demonstration.", + "This is a sample exam generated for testing purposes. " + "Bloom's levels included for demonstration.", styles['Italic'] ) elements.append(footer) - + # Build PDF doc.build(elements) - print(f"✓ Created {filename}") + print("✓ Created {filename}".format(filename=filename)) def main(): """Generate all sample exam PDFs.""" print("Generating sample exam PDFs...\n") - - # Ensure input directory exists - ensure_directories() - + # Check if reportlab is available - try: - import reportlab - except ImportError: + if not REPORTLAB_AVAILABLE: print("ERROR: reportlab is required to generate sample PDFs") print("Install it with: pip install reportlab") return 1 - + + # Ensure input directory exists + ensure_directories() + # Generate each exam for filename, exam_data in SAMPLE_EXAMS.items(): try: create_sample_pdf(filename, exam_data) except Exception as e: - print(f"✗ Failed to create {filename}: {e}") - - print(f"\n✓ Successfully generated {len(SAMPLE_EXAMS)} sample exam PDFs") - print(f"✓ Files saved to: {get_input_path('')}") + print("✗ Failed to create {filename}: {error}".format( + filename=filename, error=e)) + + print("\n✓ Successfully generated {count} sample exam PDFs".format( + count=len(SAMPLE_EXAMS))) + print("✓ Files saved to: {path}".format(path=get_input_path(''))) print("\nYou can now run: python demo.py") return 0 diff --git a/demo.py b/demo.py index a16b0af..9b97b5b 100644 --- a/demo.py +++ b/demo.py @@ -12,7 +12,7 @@ # Place your exam PDFs in the input/ folder export GOOGLE_API_KEY=your_api_key_here python demo.py - + # Results will be saved to output/ folder: # - output/charts/ - Visualization charts # - output/logs/ - Log files @@ -35,7 +35,7 @@ from profiler_agent.agent import root_agent from profiler_agent.observability import setup_logging, metrics, tracer, log_agent_event from profiler_agent.memory import MemoryBank -from profiler_agent.paths import get_input_path, get_output_path, list_input_files, ensure_directories +from profiler_agent.paths import get_input_path, list_input_files, ensure_directories from google.genai import types as genai_types @@ -44,13 +44,13 @@ async def demo_basic_workflow(): print("\n" + "="*80) print("DEMO 1: Basic Agent Workflow") print("="*80) - + # Ensure directories exist ensure_directories() - + # Setup logging with file output logger = setup_logging(level="INFO", structured=False, log_file="demo_run.log") - + # Initialize session service session_service = InMemorySessionService() await session_service.create_session( @@ -58,29 +58,29 @@ async def demo_basic_workflow(): user_id="demo_user", session_id="demo_session_1" ) - + # Initialize runner runner = Runner( agent=root_agent, app_name="professor_profiler", session_service=session_service ) - + # Create sample PDF in input folder if doesn't exist sample_pdf = get_input_path("physics_2024.pdf") if not sample_pdf.exists(): print(f"\n⚠️ Creating mock PDF at {sample_pdf}") with open(sample_pdf, "w") as f: f.write("Mock PDF content for testing") - + # Run agent with query (use just the filename, tool will look in input/) query = "Analyze the exam paper physics_2024.pdf and tell me what topics to focus on." print(f"\n📝 Query: {query}") print("\n🤖 Agent Response:") print("-" * 80) - + log_agent_event(logger, "query_start", "professor_profiler_agent", query=query) - + async for event in runner.run_async( user_id="demo_user", session_id="demo_session_1", @@ -93,7 +93,7 @@ async def demo_basic_workflow(): response_text = event.content.parts[0].text print(f"\n{response_text}") log_agent_event(logger, "query_complete", "professor_profiler_agent") - + # Show session stats stats = session_service.get_stats() print(f"\n📊 Session Stats: {json.dumps(stats, indent=2)}") @@ -104,14 +104,14 @@ async def demo_memory_bank(): print("\n" + "="*80) print("DEMO 2: Memory Bank & Long-term Context") print("="*80) - + # Memory bank will automatically save to output/memory_bank.json memory_bank = MemoryBank() user_id = "demo_user" - + # Add memories print("\n💾 Adding memories to memory bank...") - + memory_bank.add_memory( user_id=user_id, memory_type="exam_analysis", @@ -123,7 +123,7 @@ async def demo_memory_bank(): }, tags=["physics", "2024", "midterm"] ) - + memory_bank.add_memory( user_id=user_id, memory_type="study_plan", @@ -135,7 +135,7 @@ async def demo_memory_bank(): }, tags=["physics", "study_plan"] ) - + memory_bank.add_memory( user_id=user_id, memory_type="preference", @@ -146,29 +146,36 @@ async def demo_memory_bank(): }, tags=["preferences", "learning_style"] ) - + # Retrieve memories print("\n📚 Retrieving memories...") memories = memory_bank.get_memories(user_id, limit=10) for mem in memories: - print(f" - [{mem['type']}] {json.dumps(mem['content'], indent=2)}") - + print(" - [{type}] {content}".format( + type=mem['type'], + content=json.dumps(mem['content'], indent=2) + )) + # Search memories print("\n🔍 Searching for 'quantum'...") results = memory_bank.search_memories(user_id, "quantum") for result in results: - print(f" - Found: {result['type']} - {result['content']}") - + print(" - Found: {type} - {content}".format( + type=result['type'], + content=result['content'] + )) + # Get summary summary = memory_bank.get_summary(user_id) - print(f"\n📋 Memory Summary: {json.dumps(summary, indent=2)}") - + print("\n📋 Memory Summary: {summary}".format(summary=json.dumps(summary, indent=2))) + # Compact context for LLM context = memory_bank.compact_context(user_id, max_tokens=500) - print(f"\n📄 Compacted Context (for LLM):\n{context}") - - # Cleanup - os.remove("demo_memory.json") + print("\n📄 Compacted Context (for LLM):\n{context}".format(context=context)) + + # Cleanup - clear user memories instead of removing file + # (file is shared in output/memory_bank.json) + memory_bank.clear_user_memories(user_id) async def demo_observability(): @@ -176,43 +183,43 @@ async def demo_observability(): print("\n" + "="*80) print("DEMO 3: Observability (Logging, Tracing, Metrics)") print("="*80) - - # Setup structured logging - logger = setup_logging(level="INFO", structured=True) - + + # Setup structured logging (logger returned for potential future use) + setup_logging(level="INFO", structured=True) + # Start trace print("\n🔍 Starting trace for agent operation...") trace_id = tracer.start_trace("demo_agent_execution", metadata={"user": "demo"}) - + # Simulate agent operations import time - + print(" ⏱️ Simulating PDF ingestion...") time.sleep(0.1) tracer.add_span(trace_id, "pdf_ingestion", 100.5, {"file": "sample.pdf"}) metrics.increment("pdf.ingested") metrics.histogram("pdf.pages", 12) - + print(" ⏱️ Simulating question classification...") time.sleep(0.15) tracer.add_span(trace_id, "question_classification", 150.2, {"count": 25}) metrics.increment("questions.classified", 25) metrics.histogram("classification.duration_ms", 150.2) - + print(" ⏱️ Simulating trend analysis...") time.sleep(0.2) tracer.add_span(trace_id, "trend_analysis", 200.7, {"trends_found": 3}) metrics.increment("trends.analyzed") metrics.histogram("analysis.duration_ms", 200.7) - + # End trace trace_data = tracer.end_trace(trace_id) - print(f"\n📊 Trace Data:\n{json.dumps(trace_data, indent=2)}") - + print("\n📊 Trace Data:\n{trace}".format(trace=json.dumps(trace_data, indent=2))) + # Get metrics metrics_data = metrics.get_metrics() - print(f"\n📈 Metrics:\n{json.dumps(metrics_data, indent=2)}") - + print("\n📈 Metrics:\n{metrics}".format(metrics=json.dumps(metrics_data, indent=2))) + # Reset for clean slate metrics.reset() @@ -222,37 +229,37 @@ async def demo_tools(): print("\n" + "="*80) print("DEMO 4: Custom Tools (PDF, Statistics, Visualization)") print("="*80) - + from profiler_agent.tools import ( read_pdf_content, analyze_statistics, visualize_trends, list_available_exams ) - + # List available exams - print(f"\n📂 Listing available exams in input/ folder...") + print("\n📂 Listing available exams in input/ folder...") available = list_available_exams() if available.get("count", 0) > 0: - print(f" ✅ Found {available['count']} exam(s):") + print(" ✅ Found {count} exam(s):".format(count=available['count'])) for filename in available.get("files", []): - print(f" - {filename}") + print(" - {filename}".format(filename=filename)) else: - print(f" ⚠️ No exams found in input/ folder") - + print(" ⚠️ No exams found in input/ folder") + # Create mock PDF in input folder test_pdf = get_input_path("demo_exam.pdf") if not test_pdf.exists(): - print(f"\n📄 Creating mock PDF at {test_pdf}...") + print("\n📄 Creating mock PDF at {path}...".format(path=test_pdf)) with open(test_pdf, "w") as f: f.write("Mock exam content") - - print(f"\n📄 Testing read_pdf_content tool...") + + print("\n📄 Testing read_pdf_content tool...") result = read_pdf_content("demo_exam.pdf") # Just use filename - print(f" ✅ Extracted content from: {result.get('filename', 'unknown')}") - + print(" ✅ Extracted content from: {filename}".format(filename=result.get('filename', 'unknown'))) + # Test statistics tool - print(f"\n📊 Testing analyze_statistics tool...") + print("\n📊 Testing analyze_statistics tool...") mock_questions = { "questions": [ {"topic": "Quantum Mechanics", "bloom_level": "Analyze"}, @@ -262,19 +269,19 @@ async def demo_tools(): {"topic": "Quantum Mechanics", "bloom_level": "Analyze"}, ] } - + stats = analyze_statistics(json.dumps(mock_questions)) - print(f" ✅ Statistics:\n{json.dumps(stats, indent=4)}") - + print(" ✅ Statistics:\n{stats}".format(stats=json.dumps(stats, indent=4))) + # Test visualization tool (output will go to output/charts/) - print(f"\n📈 Testing visualize_trends tool...") + print("\n📈 Testing visualize_trends tool...") chart_path = "demo_chart.png" # Will be saved to output/charts/ viz_result = visualize_trends(json.dumps(stats), chart_path) - + if viz_result.get("success"): - print(f" ✅ Chart created: {viz_result['chart_path']}") + print(" ✅ Chart created: {path}".format(path=viz_result['chart_path'])) else: - print(f" ⚠️ {viz_result.get('error', 'Unknown error')}") + print(" ⚠️ {error}".format(error=viz_result.get('error', 'Unknown error'))) async def demo_multi_agent(): @@ -282,32 +289,32 @@ async def demo_multi_agent(): print("\n" + "="*80) print("DEMO 5: Multi-Agent System (Hub-and-Spoke)") print("="*80) - + from profiler_agent.sub_agents import taxonomist, trend_spotter, strategist - - print(f"\n🤖 Root Agent: {root_agent.name}") - print(f" Model: {root_agent.model}") - print(f" Description: {root_agent.description}") - print(f" Tools: {[tool.name for tool in root_agent.tools]}") - print(f" Sub-agents: {[agent.name for agent in root_agent.sub_agents]}") - - print(f"\n🔹 Sub-agent 1: {taxonomist.name}") - print(f" Model: {taxonomist.model}") - print(f" Role: {taxonomist.description}") - print(f" Output Key: {taxonomist.output_key}") - - print(f"\n🔹 Sub-agent 2: {trend_spotter.name}") - print(f" Model: {trend_spotter.model}") - print(f" Role: {trend_spotter.description}") - print(f" Output Key: {trend_spotter.output_key}") - - print(f"\n🔹 Sub-agent 3: {strategist.name}") - print(f" Model: {strategist.model}") - print(f" Role: {strategist.description}") - print(f" Output Key: {strategist.output_key}") - - print(f"\n📊 Architecture Pattern: Hub-and-Spoke (Sequential Execution)") - print(f" Flow: Root → Taxonomist → Trend Spotter → Strategist") + + print("\n🤖 Root Agent: {name}".format(name=root_agent.name)) + print(" Model: {model}".format(model=root_agent.model)) + print(" Description: {description}".format(description=root_agent.description)) + print(" Tools: {tools}".format(tools=[tool.name for tool in root_agent.tools])) + print(" Sub-agents: {agents}".format(agents=[agent.name for agent in root_agent.sub_agents])) + + print("\n🔹 Sub-agent 1: {name}".format(name=taxonomist.name)) + print(" Model: {model}".format(model=taxonomist.model)) + print(" Role: {description}".format(description=taxonomist.description)) + print(" Output Key: {output_key}".format(output_key=taxonomist.output_key)) + + print("\n🔹 Sub-agent 2: {name}".format(name=trend_spotter.name)) + print(" Model: {model}".format(model=trend_spotter.model)) + print(" Role: {description}".format(description=trend_spotter.description)) + print(" Output Key: {output_key}".format(output_key=trend_spotter.output_key)) + + print("\n🔹 Sub-agent 3: {name}".format(name=strategist.name)) + print(" Model: {model}".format(model=strategist.model)) + print(" Role: {description}".format(description=strategist.description)) + print(" Output Key: {output_key}".format(output_key=strategist.output_key)) + + print("\n📊 Architecture Pattern: Hub-and-Spoke (Sequential Execution)") + print(" Flow: Root → Taxonomist → Trend Spotter → Strategist") async def main(): @@ -321,7 +328,7 @@ async def main(): print(" ✅ Sessions & Memory (InMemorySessionService + MemoryBank)") print(" ✅ Observability (Logging, Tracing, Metrics)") print(" ✅ Gemini API integration (if API key provided)") - + # Show folder structure print("\n📁 Project Structure:") print(" 📂 input/ - Place your exam PDFs here") @@ -329,27 +336,27 @@ async def main(): print(" ├── charts/ - Visualization charts") print(" ├── logs/ - Log files") print(" └── reports/ - Analysis reports") - + # Ensure directories exist ensure_directories() - + # Check for input files input_files = list_input_files() - print(f"\n📄 Found {len(input_files)} PDF file(s) in input/ folder") + print("\n📄 Found {count} PDF file(s) in input/ folder".format(count=len(input_files))) if input_files: for f in input_files[:3]: # Show first 3 - print(f" - {f.name}") + print(" - {name}".format(name=f.name)) if len(input_files) > 3: - print(f" ... and {len(input_files) - 3} more") - + print(" ... and {count} more".format(count=len(input_files) - 3)) + # Check for API key api_key = os.getenv("GOOGLE_API_KEY") if not api_key: print("\n⚠️ WARNING: GOOGLE_API_KEY not set. Agent will use mock responses.") print(" To use real Gemini API, set: export GOOGLE_API_KEY=your_key") else: - print(f"\n✅ GOOGLE_API_KEY found (length: {len(api_key)})") - + print("\n✅ GOOGLE_API_KEY found (length: {length})".format(length=len(api_key))) + try: # Run demos await demo_multi_agent() @@ -357,19 +364,19 @@ async def main(): await demo_observability() await demo_memory_bank() await demo_basic_workflow() - + print("\n" + "="*80) print("✅ ALL DEMOS COMPLETED SUCCESSFULLY!") print("="*80) - print(f"\n📊 Check the output/ folder for:") - print(f" - Charts: output/charts/") - print(f" - Logs: output/logs/demo_run.log") - print(f" - Memory: output/memory_bank.json") - + print("\n📊 Check the output/ folder for:") + print(" - Charts: output/charts/") + print(" - Logs: output/logs/demo_run.log") + print(" - Memory: output/memory_bank.json") + except KeyboardInterrupt: print("\n\n⚠️ Demo interrupted by user") except Exception as e: - print(f"\n\n❌ Error during demo: {e}") + print("\n\n❌ Error during demo: {error}".format(error=e)) import traceback traceback.print_exc() diff --git a/google/__pycache__/__init__.cpython-312.pyc b/google/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 7ec3593..0000000 Binary files a/google/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/google/adk/__pycache__/__init__.cpython-312.pyc b/google/adk/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 4f5ce88..0000000 Binary files a/google/adk/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/google/adk/agents/__pycache__/__init__.cpython-312.pyc b/google/adk/agents/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 5dd09dc..0000000 Binary files a/google/adk/agents/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/google/adk/agents/__pycache__/agent.cpython-312.pyc b/google/adk/agents/__pycache__/agent.cpython-312.pyc deleted file mode 100644 index beab676..0000000 Binary files a/google/adk/agents/__pycache__/agent.cpython-312.pyc and /dev/null differ diff --git a/google/adk/agents/__pycache__/callback_context.cpython-312.pyc b/google/adk/agents/__pycache__/callback_context.cpython-312.pyc deleted file mode 100644 index 07317f1..0000000 Binary files a/google/adk/agents/__pycache__/callback_context.cpython-312.pyc and /dev/null differ diff --git a/google/adk/agents/agent.py b/google/adk/agents/agent.py index cd9062d..2f416a1 100644 --- a/google/adk/agents/agent.py +++ b/google/adk/agents/agent.py @@ -21,7 +21,7 @@ class Agent: - Delegate work to sub-agents - Post-process results via callbacks """ - + def __init__( self, name: str, @@ -37,30 +37,30 @@ def __init__( # Agent identity and model configuration self.name = name self.model = model - + # High-level role and behavioral instructions self.description = description self.instruction = instruction - + # Execution extensions self.tools = tools or [] self.sub_agents = sub_agents or [] - + # Output routing / post-processing metadata self.output_key = output_key self.after_agent_callback = after_agent_callback - + # Model generation parameters (temperature, top_p, etc.) self.kwargs = kwargs - + # Runtime state (initialized later) self.client = None self.context = {} - + def __repr__(self): """Readable representation for debugging and logs.""" return f"Agent(name='{self.name}', model='{self.model}')" - + async def initialize(self, client): """Attach a Gemini client to this agent and all sub-agents. @@ -70,7 +70,7 @@ async def initialize(self, client): self.client = client for sub_agent in self.sub_agents: await sub_agent.initialize(client) - + async def run( self, prompt: str, @@ -88,19 +88,19 @@ async def run( """ if not self.client: raise RuntimeError(f"Agent {self.name} not initialized with client") - + # Store execution-scoped context self.context = context or {} - + # Construct system-level instructions (role, constraints, metadata) system_instruction = self._build_system_instruction() - + # Combine user prompt with structured context full_prompt = self._build_full_prompt(prompt) - + # Prepare Gemini-compatible tool declarations tool_config = self._prepare_tool_config() - + try: # Execute the primary LLM call response = await self._execute_llm( @@ -108,11 +108,11 @@ async def run( system_instruction, tool_config ) - + # Sequentially run sub-agents on the parent response if self.sub_agents: response = await self._execute_sub_agents(response) - + # Optional post-processing hook if self.after_agent_callback: from .callback_context import CallbackContext @@ -120,14 +120,14 @@ async def run( callback_result = self.after_agent_callback(ctx) if callback_result: response = callback_result - + # Standardized agent output envelope return { "agent": self.name, "response": response, "output_key": self.output_key } - + except Exception as e: # Fail safely without crashing the agent runtime return { @@ -135,7 +135,7 @@ async def run( "error": str(e), "output_key": self.output_key } - + def _build_system_instruction(self) -> str: """Assemble the system instruction passed to the LLM. @@ -146,23 +146,23 @@ def _build_system_instruction(self) -> str: - Sub-agent awareness """ parts = [] - + if self.description: parts.append(f"Role: {self.description}") - + if self.instruction: parts.append(f"Instructions: {self.instruction}") - + if self.tools: tool_names = [getattr(t, 'name', 'tool') for t in self.tools] parts.append(f"Available tools: {', '.join(tool_names)}") - + if self.sub_agents: agent_names = [a.name for a in self.sub_agents] parts.append(f"Sub-agents: {', '.join(agent_names)}") - + return "\n\n".join(parts) - + def _build_full_prompt(self, prompt: str) -> str: """Merge the user prompt with structured execution context. @@ -170,16 +170,16 @@ def _build_full_prompt(self, prompt: str) -> str: """ if not self.context: return prompt - + context_str = "\n\nContext:\n" for key, value in self.context.items(): if isinstance(value, (dict, list)): context_str += f"- {key}: {json.dumps(value, indent=2)}\n" else: context_str += f"- {key}: {value}\n" - + return prompt + context_str - + def _prepare_tool_config(self) -> Optional[Dict[str, Any]]: """Convert registered tools into Gemini function declarations. @@ -187,19 +187,19 @@ def _prepare_tool_config(self) -> Optional[Dict[str, Any]]: """ if not self.tools: return None - + tool_declarations = [] for tool in self.tools: if hasattr(tool, 'to_gemini_declaration'): tool_declarations.append(tool.to_gemini_declaration()) - + if not tool_declarations: return None - + return { "function_declarations": tool_declarations } - + async def _execute_llm( self, prompt: str, @@ -214,7 +214,7 @@ async def _execute_llm( - Tool call detection and execution """ from google import genai - + # Model generation parameters config = { "temperature": self.kwargs.get("temperature", 0.7), @@ -222,7 +222,7 @@ async def _execute_llm( "top_k": self.kwargs.get("top_k", 40), "max_output_tokens": self.kwargs.get("max_output_tokens", 2048), } - + # User content payload contents = [ genai_types.Content( @@ -230,23 +230,23 @@ async def _execute_llm( parts=[genai_types.Part.from_text(text=prompt)] ) ] - + # Request configuration generate_kwargs = { "model": self.model, "contents": contents, "config": genai_types.GenerateContentConfig(**config) } - + if system_instruction: generate_kwargs["config"].system_instruction = system_instruction - + if tool_config: generate_kwargs["config"].tools = [tool_config] - + # Invoke Gemini response = await self.client.aio.models.generate_content(**generate_kwargs) - + # Inspect model output for tool calls or text responses if hasattr(response, 'candidates') and response.candidates: candidate = response.candidates[0] @@ -255,12 +255,12 @@ async def _execute_llm( if hasattr(part, 'function_call') and part.function_call: # Execute the requested tool return await self._execute_tool_call(part.function_call) - + # Default to returning textual output return candidate.content.parts[0].text - + return str(response.text) if hasattr(response, 'text') else "" - + async def _execute_tool_call(self, function_call) -> str: """Execute a tool invoked by the LLM. @@ -269,7 +269,7 @@ async def _execute_tool_call(self, function_call) -> str: """ function_name = function_call.name args = dict(function_call.args) if hasattr(function_call, 'args') else {} - + for tool in self.tools: if hasattr(tool, 'name') and tool.name == function_name: if hasattr(tool, 'execute'): @@ -278,9 +278,9 @@ async def _execute_tool_call(self, function_call) -> str: elif hasattr(tool, 'func'): result = tool.func(**args) return json.dumps(result) - + return json.dumps({"error": f"Tool {function_name} not found"}) - + async def _execute_sub_agents(self, parent_response: str) -> str: """Run sub-agents sequentially using the parent agent's response. @@ -288,7 +288,7 @@ async def _execute_sub_agents(self, parent_response: str) -> str: previous agent’s output as its prompt. """ results = [f"[{self.name} Initial Response]\n{parent_response}"] - + for sub_agent in self.sub_agents: result = await sub_agent.run( prompt=parent_response, @@ -296,5 +296,5 @@ async def _execute_sub_agents(self, parent_response: str) -> str: ) response_text = result.get("response", "") results.append(f"\n[{sub_agent.name} Response]\n{response_text}") - + return "\n".join(results) diff --git a/google/adk/agents/callback_context.py b/google/adk/agents/callback_context.py index aa15f05..17b6705 100644 --- a/google/adk/agents/callback_context.py +++ b/google/adk/agents/callback_context.py @@ -5,7 +5,7 @@ class CallbackContext: """Context passed to after_agent_callback functions.""" - + def __init__( self, agent: Any, @@ -15,11 +15,11 @@ def __init__( self.agent = agent self.response = response self.metadata = metadata or {} - + def get_response_text(self) -> str: """Get response as text.""" return str(self.response) - + def to_content(self) -> Content: """Convert response to Content object.""" return Content( diff --git a/google/adk/runners/__pycache__/__init__.cpython-312.pyc b/google/adk/runners/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index ce7d040..0000000 Binary files a/google/adk/runners/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/google/adk/runners/__pycache__/runner.cpython-312.pyc b/google/adk/runners/__pycache__/runner.cpython-312.pyc deleted file mode 100644 index b218eeb..0000000 Binary files a/google/adk/runners/__pycache__/runner.cpython-312.pyc and /dev/null differ diff --git a/google/adk/runners/runner.py b/google/adk/runners/runner.py index 5831a88..ddb16c5 100644 --- a/google/adk/runners/runner.py +++ b/google/adk/runners/runner.py @@ -12,7 +12,7 @@ class RunnerEvent: """Event emitted during agent execution.""" - + def __init__( self, content: Optional[genai_types.Content] = None, @@ -24,18 +24,18 @@ def __init__( self.agent_name = agent_name self._is_final = is_final self.metadata = metadata or {} - + def is_final_response(self) -> bool: """Check if this is the final response.""" return self._is_final - + def __repr__(self): return f"RunnerEvent(agent='{self.agent_name}', is_final={self._is_final})" class Runner: """Execute agents with session management and streaming.""" - + def __init__( self, agent: Any, @@ -49,14 +49,14 @@ def __init__( self.session_service = session_service self.api_key = api_key or os.getenv("GOOGLE_API_KEY") self.kwargs = kwargs - + # Initialize Gemini client if not self.api_key: logger.warning("No GOOGLE_API_KEY found, agent will use mock responses") self.client = None else: self.client = genai.Client(api_key=self.api_key) - + async def run_async( self, user_id: str, @@ -65,7 +65,7 @@ async def run_async( **kwargs ) -> AsyncIterator[RunnerEvent]: """Execute agent and stream results.""" - + # Extract message text message_text = "" if new_message and hasattr(new_message, "parts") and len(new_message.parts) > 0: @@ -74,21 +74,21 @@ async def run_async( message_text = part.text elif hasattr(part, "from_text"): message_text = str(part) - + logger.info(f"Running agent {self.agent.name} for session {session_id}") logger.debug(f"Message: {message_text[:100]}...") - + # Get or create session session = await self.session_service.get_session( app_name=self.app_name, user_id=user_id, session_id=session_id ) - + # Initialize agent with client if self.client: await self.agent.initialize(self.client) - + # Execute agent try: # Yield intermediate events @@ -100,7 +100,7 @@ async def run_async( agent_name=self.agent.name, is_final=False ) - + # Run agent if self.client: result = await self.agent.run( @@ -111,7 +111,7 @@ async def run_async( else: # Mock response if no API key response_text = f"[Mock Response] {self.agent.name} processed: {message_text[:100]}" - + # Update session with result if session: await self.session_service.add_message( @@ -128,7 +128,7 @@ async def run_async( role="assistant", content=response_text ) - + # Yield final response yield RunnerEvent( content=genai_types.Content( @@ -139,7 +139,7 @@ async def run_async( is_final=True, metadata={"session_id": session_id} ) - + except Exception as e: logger.error(f"Error running agent: {e}", exc_info=True) yield RunnerEvent( diff --git a/google/adk/sessions/__pycache__/__init__.cpython-312.pyc b/google/adk/sessions/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 6327005..0000000 Binary files a/google/adk/sessions/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/google/adk/sessions/__pycache__/in_memory_session_service.cpython-312.pyc b/google/adk/sessions/__pycache__/in_memory_session_service.cpython-312.pyc deleted file mode 100644 index 5dd0f08..0000000 Binary files a/google/adk/sessions/__pycache__/in_memory_session_service.cpython-312.pyc and /dev/null differ diff --git a/google/adk/sessions/in_memory_session_service.py b/google/adk/sessions/in_memory_session_service.py index 35a0884..3ca2e95 100644 --- a/google/adk/sessions/in_memory_session_service.py +++ b/google/adk/sessions/in_memory_session_service.py @@ -11,13 +11,13 @@ class InMemorySessionService: """Manage sessions and conversation history in memory.""" - + def __init__(self): # sessions: {app_name: {user_id: {session_id: session_data}}} self._sessions: Dict[str, Dict[str, Dict[str, Dict[str, Any]]]] = defaultdict( lambda: defaultdict(dict) ) - + async def create_session( self, app_name: str, @@ -36,12 +36,12 @@ async def create_session( "context": metadata or {}, "metadata": metadata or {} } - + self._sessions[app_name][user_id][session_id] = session_data logger.info(f"Created session {session_id} for user {user_id}") - + return session_data - + async def get_session( self, app_name: str, @@ -52,9 +52,9 @@ async def get_session( if session_id not in self._sessions[app_name][user_id]: logger.info(f"Session {session_id} not found, creating new one") return await self.create_session(app_name, user_id, session_id) - + return self._sessions[app_name][user_id][session_id] - + async def update_session( self, app_name: str, @@ -65,13 +65,13 @@ async def update_session( """Update session data.""" if session_id not in self._sessions[app_name][user_id]: raise ValueError(f"Session {session_id} not found") - + session = self._sessions[app_name][user_id][session_id] session.update(updates) session["updated_at"] = datetime.now().isoformat() - + return session - + async def add_message( self, app_name: str, @@ -83,21 +83,21 @@ async def add_message( ) -> Dict[str, Any]: """Add a message to session history.""" session = await self.get_session(app_name, user_id, session_id) - + message = { "role": role, "content": content, "timestamp": datetime.now().isoformat(), "metadata": metadata or {} } - + session["messages"].append(message) session["updated_at"] = datetime.now().isoformat() - + logger.debug(f"Added {role} message to session {session_id}") - + return message - + async def get_messages( self, app_name: str, @@ -107,17 +107,17 @@ async def get_messages( ) -> List[Dict[str, Any]]: """Get conversation history for a session.""" session = await self.get_session(app_name, user_id, session_id) - + if not session: return [] - + messages = session.get("messages", []) - + if limit: return messages[-limit:] - + return messages - + async def delete_session( self, app_name: str, @@ -129,9 +129,9 @@ async def delete_session( del self._sessions[app_name][user_id][session_id] logger.info(f"Deleted session {session_id}") return True - + return False - + async def list_sessions( self, app_name: str, @@ -140,7 +140,7 @@ async def list_sessions( """List all sessions for a user.""" sessions = list(self._sessions[app_name][user_id].values()) return sorted(sessions, key=lambda s: s["updated_at"], reverse=True) - + async def update_context( self, app_name: str, @@ -150,15 +150,15 @@ async def update_context( ) -> Dict[str, Any]: """Update session context (for memory/state management).""" session = await self.get_session(app_name, user_id, session_id) - + if "context" not in session: session["context"] = {} - + session["context"].update(context_updates) session["updated_at"] = datetime.now().isoformat() - + return session["context"] - + async def get_context( self, app_name: str, @@ -168,7 +168,7 @@ async def get_context( """Get session context.""" session = await self.get_session(app_name, user_id, session_id) return session.get("context", {}) if session else {} - + def get_stats(self) -> Dict[str, Any]: """Get service statistics.""" total_sessions = sum( @@ -176,14 +176,14 @@ def get_stats(self) -> Dict[str, Any]: for app in self._sessions.values() for users_sessions in app.values() ) - + total_messages = sum( len(session.get("messages", [])) for app in self._sessions.values() for users_sessions in app.values() for session in users_sessions.values() ) - + return { "total_sessions": total_sessions, "total_messages": total_messages, diff --git a/google/adk/tools/__pycache__/__init__.cpython-312.pyc b/google/adk/tools/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index ab6dfad..0000000 Binary files a/google/adk/tools/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/google/adk/tools/__pycache__/function_tool.cpython-312.pyc b/google/adk/tools/__pycache__/function_tool.cpython-312.pyc deleted file mode 100644 index 584b6e0..0000000 Binary files a/google/adk/tools/__pycache__/function_tool.cpython-312.pyc and /dev/null differ diff --git a/google/adk/tools/function_tool.py b/google/adk/tools/function_tool.py index 6c532d4..5a5b756 100644 --- a/google/adk/tools/function_tool.py +++ b/google/adk/tools/function_tool.py @@ -5,7 +5,7 @@ class FunctionTool: """Wrapper for Python functions to be used as LLM tools.""" - + def __init__( self, func: Optional[Callable] = None, @@ -17,7 +17,7 @@ def __init__( self.name = name or (func.__name__ if func else kwargs.get('name', 'tool')) self.description = description or (func.__doc__ if func else kwargs.get('description', '')) self.kwargs = kwargs - + # Parse function signature if available if self.func: self.signature = inspect.signature(self.func) @@ -25,17 +25,17 @@ def __init__( else: self.signature = None self.parameters = {} - + def _extract_parameters(self) -> Dict[str, Any]: """Extract parameter schema from function signature.""" params = {} - + for param_name, param in self.signature.parameters.items(): param_info = { "type": "string", # Default to string "description": f"Parameter {param_name}" } - + # Try to infer type from annotation if param.annotation != inspect.Parameter.empty: annotation = param.annotation @@ -51,18 +51,18 @@ def _extract_parameters(self) -> Dict[str, Any]: param_info["type"] = "object" elif annotation == list: param_info["type"] = "array" - + # Check if required if param.default == inspect.Parameter.empty: param_info["required"] = True else: param_info["required"] = False param_info["default"] = param.default - + params[param_name] = param_info - + return params - + def to_gemini_declaration(self) -> Dict[str, Any]: """Convert to Gemini function declaration format.""" # Extract required parameters @@ -70,7 +70,7 @@ def to_gemini_declaration(self) -> Dict[str, Any]: name for name, info in self.parameters.items() if info.get("required", False) ] - + # Build parameter schema properties = {} for name, info in self.parameters.items(): @@ -78,7 +78,7 @@ def to_gemini_declaration(self) -> Dict[str, Any]: "type": info["type"], "description": info["description"] } - + declaration = { "name": self.name, "description": self.description or f"Execute {self.name} function", @@ -87,29 +87,29 @@ def to_gemini_declaration(self) -> Dict[str, Any]: "properties": properties, } } - + if required_params: declaration["parameters"]["required"] = required_params - + return declaration - + async def execute(self, **kwargs) -> Any: """Execute the wrapped function.""" if not self.func: raise RuntimeError(f"No function defined for tool {self.name}") - + # Check if function is async if inspect.iscoroutinefunction(self.func): return await self.func(**kwargs) else: return self.func(**kwargs) - + def __call__(self, **kwargs) -> Any: """Allow tool to be called directly.""" if inspect.iscoroutinefunction(self.func): import asyncio return asyncio.create_task(self.execute(**kwargs)) return self.func(**kwargs) - + def __repr__(self): return f"FunctionTool(name='{self.name}')" diff --git a/profiler_agent/__pycache__/__init__.cpython-312.pyc b/profiler_agent/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 186b80d..0000000 Binary files a/profiler_agent/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/__pycache__/agent.cpython-312.pyc b/profiler_agent/__pycache__/agent.cpython-312.pyc deleted file mode 100644 index f48b968..0000000 Binary files a/profiler_agent/__pycache__/agent.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/__pycache__/agent_utils.cpython-312.pyc b/profiler_agent/__pycache__/agent_utils.cpython-312.pyc deleted file mode 100644 index e60a4c7..0000000 Binary files a/profiler_agent/__pycache__/agent_utils.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/__pycache__/config.cpython-312.pyc b/profiler_agent/__pycache__/config.cpython-312.pyc deleted file mode 100644 index 4a5c635..0000000 Binary files a/profiler_agent/__pycache__/config.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/__pycache__/memory.cpython-312.pyc b/profiler_agent/__pycache__/memory.cpython-312.pyc deleted file mode 100644 index 631a370..0000000 Binary files a/profiler_agent/__pycache__/memory.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/__pycache__/observability.cpython-312.pyc b/profiler_agent/__pycache__/observability.cpython-312.pyc deleted file mode 100644 index 27a61e8..0000000 Binary files a/profiler_agent/__pycache__/observability.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/__pycache__/tools.cpython-312.pyc b/profiler_agent/__pycache__/tools.cpython-312.pyc deleted file mode 100644 index 5bc5812..0000000 Binary files a/profiler_agent/__pycache__/tools.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/agent_utils.py b/profiler_agent/agent_utils.py index dcc85e3..4c374f2 100644 --- a/profiler_agent/agent_utils.py +++ b/profiler_agent/agent_utils.py @@ -1,5 +1,6 @@ from google.adk.agents.callback_context import CallbackContext from google.genai.types import Content + def suppress_output_callback(callback_context: CallbackContext) -> Content: return Content() diff --git a/profiler_agent/config.py b/profiler_agent/config.py index a3bf186..885e6c9 100644 --- a/profiler_agent/config.py +++ b/profiler_agent/config.py @@ -5,7 +5,7 @@ try: _, project_id = google.auth.default() except Exception: - # If Application Default Credentials are not available (e.g. in CI/local test) + # If Application Default Credentials are not available (e.g. in CI/local) # fall back to the env var or a sensible default so importing this module # doesn't raise. Tests can override `GOOGLE_CLOUD_PROJECT` if needed project_id = os.environ.get("GOOGLE_CLOUD_PROJECT", "local") @@ -13,9 +13,11 @@ os.environ.setdefault("GOOGLE_CLOUD_LOCATION", "global") os.environ.setdefault("GOOGLE_GENAI_USE_VERTEXAI", "True") + @dataclass class ProfilerConfiguration: classifier_model: str = "gemini-2.0-flash-exp" analyzer_model: str = "gemini-2.0-pro-exp" + config = ProfilerConfiguration() diff --git a/profiler_agent/memory.py b/profiler_agent/memory.py index dd0176f..6538cfd 100644 --- a/profiler_agent/memory.py +++ b/profiler_agent/memory.py @@ -10,7 +10,7 @@ class MemoryBank: """Long-term memory storage for agent context.""" - + def __init__(self, storage_path: Optional[str] = None): if storage_path is None: # Default to output directory @@ -18,7 +18,7 @@ def __init__(self, storage_path: Optional[str] = None): self.storage_path = storage_path self.memories: Dict[str, List[Dict[str, Any]]] = defaultdict(list) self.load() - + def load(self): """Load memories from disk.""" if os.path.exists(self.storage_path): @@ -28,7 +28,7 @@ def load(self): self.memories = defaultdict(list, data) except Exception as e: print(f"Warning: Failed to load memory bank: {e}") - + def save(self): """Persist memories to disk.""" try: @@ -36,7 +36,7 @@ def save(self): json.dump(dict(self.memories), f, indent=2) except Exception as e: print(f"Warning: Failed to save memory bank: {e}") - + def add_memory( self, user_id: str, @@ -46,18 +46,18 @@ def add_memory( ) -> str: """ Add a new memory. - + Args: user_id: User identifier memory_type: Type of memory (e.g., 'exam_analysis', 'study_plan', 'preference') content: Memory content tags: Optional tags for categorization - + Returns: Memory ID """ memory_id = self._generate_id(user_id, memory_type, content) - + memory = { "id": memory_id, "user_id": user_id, @@ -68,12 +68,12 @@ def add_memory( "access_count": 0, "last_accessed": None } - + self.memories[user_id].append(memory) self.save() - + return memory_id - + def get_memories( self, user_id: str, @@ -83,41 +83,41 @@ def get_memories( ) -> List[Dict[str, Any]]: """ Retrieve memories for a user. - + Args: user_id: User identifier memory_type: Filter by memory type tags: Filter by tags (must have all tags) limit: Maximum number of memories to return - + Returns: List of matching memories """ user_memories = self.memories.get(user_id, []) - + # Filter by type if memory_type: user_memories = [m for m in user_memories if m["type"] == memory_type] - + # Filter by tags if tags: user_memories = [ m for m in user_memories if all(tag in m.get("tags", []) for tag in tags) ] - + # Sort by most recent and update access count user_memories.sort(key=lambda m: m["created_at"], reverse=True) - + # Update access count for returned memories for memory in user_memories[:limit]: memory["access_count"] += 1 memory["last_accessed"] = datetime.now().isoformat() - + self.save() - + return user_memories[:limit] - + def search_memories( self, user_id: str, @@ -126,34 +126,34 @@ def search_memories( ) -> List[Dict[str, Any]]: """ Search memories by text content. - + Args: user_id: User identifier query: Search query limit: Maximum number of results - + Returns: List of matching memories """ user_memories = self.memories.get(user_id, []) query_lower = query.lower() - + # Simple text-based search matches = [] for memory in user_memories: content_str = json.dumps(memory["content"]).lower() tags_str = " ".join(memory.get("tags", [])).lower() - + if query_lower in content_str or query_lower in tags_str: # Calculate simple relevance score score = content_str.count(query_lower) + tags_str.count(query_lower) * 2 matches.append((score, memory)) - + # Sort by relevance matches.sort(key=lambda x: x[0], reverse=True) - + return [m for _, m in matches[:limit]] - + def update_memory( self, user_id: str, @@ -162,12 +162,12 @@ def update_memory( ) -> bool: """ Update an existing memory. - + Args: user_id: User identifier memory_id: Memory to update updates: Fields to update - + Returns: True if memory was found and updated """ @@ -179,30 +179,30 @@ def update_memory( memory["updated_at"] = datetime.now().isoformat() self.save() return True - + return False - + def delete_memory(self, user_id: str, memory_id: str) -> bool: """Delete a memory.""" user_memories = self.memories.get(user_id, []) initial_count = len(user_memories) - + self.memories[user_id] = [m for m in user_memories if m["id"] != memory_id] - + if len(self.memories[user_id]) < initial_count: self.save() return True - + return False - + def get_summary(self, user_id: str) -> Dict[str, Any]: """Get summary statistics for user's memories.""" user_memories = self.memories.get(user_id, []) - + type_counts = defaultdict(int) for memory in user_memories: type_counts[memory["type"]] += 1 - + return { "total_memories": len(user_memories), "by_type": dict(type_counts), @@ -212,7 +212,7 @@ def get_summary(self, user_id: str) -> Dict[str, Any]: reverse=True )[:5] if user_memories else [] } - + def compact_context( self, user_id: str, @@ -221,42 +221,42 @@ def compact_context( ) -> str: """ Compact memories into a context string for LLM. - + Args: user_id: User identifier memory_types: Types of memories to include max_tokens: Approximate max tokens (rough estimate: 1 token ~= 4 chars) - + Returns: Compacted context string """ user_memories = self.get_memories(user_id, limit=20) - + # Filter by type if specified if memory_types: user_memories = [m for m in user_memories if m["type"] in memory_types] - + # Build context string context_parts = ["Historical Context:"] current_length = len(context_parts[0]) max_chars = max_tokens * 4 # Rough estimate - + for memory in user_memories: memory_str = f"\n- [{memory['type']}] {json.dumps(memory['content'])}" - + if current_length + len(memory_str) > max_chars: break - + context_parts.append(memory_str) current_length += len(memory_str) - + return "\n".join(context_parts) - + def _generate_id(self, user_id: str, memory_type: str, content: Dict) -> str: """Generate unique memory ID.""" data = f"{user_id}:{memory_type}:{json.dumps(content)}:{datetime.now().isoformat()}" - return hashlib.md5(data.encode()).hexdigest()[:16] - + return hashlib.md5(data.encode(), usedforsecurity=False).hexdigest()[:16] + def clear_user_memories(self, user_id: str) -> int: """Clear all memories for a user.""" count = len(self.memories.get(user_id, [])) diff --git a/profiler_agent/observability.py b/profiler_agent/observability.py index 4974886..f27ae75 100644 --- a/profiler_agent/observability.py +++ b/profiler_agent/observability.py @@ -8,13 +8,13 @@ from datetime import datetime from collections import defaultdict import threading -from .paths import get_output_path, LOGS_DIR +from .paths import get_output_path # Configure structured logging class StructuredFormatter(logging.Formatter): """JSON formatter for structured logs.""" - + def format(self, record: logging.LogRecord) -> str: log_data = { "timestamp": datetime.utcnow().isoformat(), @@ -25,7 +25,7 @@ def format(self, record: logging.LogRecord) -> str: "function": record.funcName, "line": record.lineno } - + # Add extra fields if present if hasattr(record, "trace_id"): log_data["trace_id"] = record.trace_id @@ -33,37 +33,37 @@ def format(self, record: logging.LogRecord) -> str: log_data["agent_name"] = record.agent_name if hasattr(record, "duration_ms"): log_data["duration_ms"] = record.duration_ms - + # Add exception info if present if record.exc_info: log_data["exception"] = self.formatException(record.exc_info) - + return json.dumps(log_data) def setup_logging(level: str = "INFO", structured: bool = False, log_file: Optional[str] = None): """ Setup logging configuration. - + Args: level: Log level (INFO, DEBUG, WARNING, ERROR) structured: Use JSON structured logging log_file: Optional log file name (saved to output/logs/) """ log_level = getattr(logging, level.upper(), logging.INFO) - + # Create logger logger = logging.getLogger() logger.setLevel(log_level) - + # Remove existing handlers for handler in logger.handlers[:]: logger.removeHandler(handler) - + # Create console handler console_handler = logging.StreamHandler() console_handler.setLevel(log_level) - + # Set formatter if structured: formatter = StructuredFormatter() @@ -72,10 +72,10 @@ def setup_logging(level: str = "INFO", structured: bool = False, log_file: Optio '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) - + console_handler.setFormatter(formatter) logger.addHandler(console_handler) - + # Add file handler if requested if log_file: log_path = get_output_path(log_file, "logs") @@ -84,21 +84,21 @@ def setup_logging(level: str = "INFO", structured: bool = False, log_file: Optio file_handler.setFormatter(formatter) logger.addHandler(file_handler) logger.info(f"Logging to file: {log_path}") - + return logger class Tracer: """Distributed tracing for agent operations.""" - + def __init__(self): self._traces: Dict[str, Dict[str, Any]] = {} self._lock = threading.Lock() - + def start_trace(self, operation: str, metadata: Optional[Dict] = None) -> str: """Start a new trace.""" trace_id = str(uuid.uuid4()) - + with self._lock: self._traces[trace_id] = { "trace_id": trace_id, @@ -107,9 +107,9 @@ def start_trace(self, operation: str, metadata: Optional[Dict] = None) -> str: "metadata": metadata or {}, "spans": [] } - + return trace_id - + def add_span( self, trace_id: str, @@ -126,19 +126,19 @@ def add_span( "timestamp": time.time(), "metadata": metadata or {} }) - + def end_trace(self, trace_id: str) -> Dict[str, Any]: """End a trace and return the trace data.""" with self._lock: if trace_id not in self._traces: return {} - + trace = self._traces[trace_id] trace["end_time"] = time.time() trace["total_duration_ms"] = (trace["end_time"] - trace["start_time"]) * 1000 - + return trace - + def get_trace(self, trace_id: str) -> Optional[Dict[str, Any]]: """Get trace data by ID.""" with self._lock: @@ -147,39 +147,39 @@ def get_trace(self, trace_id: str) -> Optional[Dict[str, Any]]: class MetricsCollector: """Collect and aggregate metrics.""" - + def __init__(self): self._counters: Dict[str, int] = defaultdict(int) self._gauges: Dict[str, float] = {} self._histograms: Dict[str, list] = defaultdict(list) self._lock = threading.Lock() - + def increment(self, metric: str, value: int = 1, tags: Optional[Dict] = None): """Increment a counter.""" key = self._make_key(metric, tags) with self._lock: self._counters[key] += value - + def gauge(self, metric: str, value: float, tags: Optional[Dict] = None): """Set a gauge value.""" key = self._make_key(metric, tags) with self._lock: self._gauges[key] = value - + def histogram(self, metric: str, value: float, tags: Optional[Dict] = None): """Add a value to histogram.""" key = self._make_key(metric, tags) with self._lock: self._histograms[key].append(value) - + def _make_key(self, metric: str, tags: Optional[Dict] = None) -> str: """Create metric key with tags.""" if not tags: return metric - + tag_str = ",".join(f"{k}={v}" for k, v in sorted(tags.items())) return f"{metric}{{{tag_str}}}" - + def get_metrics(self) -> Dict[str, Any]: """Get all metrics.""" with self._lock: @@ -194,14 +194,14 @@ def get_metrics(self) -> Dict[str, Any]: "min": min(values), "max": max(values) } - + return { "counters": dict(self._counters), "gauges": dict(self._gauges), "histograms": histogram_stats, "timestamp": datetime.utcnow().isoformat() } - + def reset(self): """Reset all metrics.""" with self._lock: @@ -222,15 +222,15 @@ def decorator(func): async def async_wrapper(*args, **kwargs): trace_id = tracer.start_trace(operation_name) start_time = time.time() - + try: result = await func(*args, **kwargs) duration_ms = (time.time() - start_time) * 1000 - + tracer.add_span(trace_id, operation_name, duration_ms, {"status": "success"}) metrics.histogram(f"{operation_name}.duration_ms", duration_ms) metrics.increment(f"{operation_name}.success") - + return result except Exception as e: duration_ms = (time.time() - start_time) * 1000 @@ -239,20 +239,20 @@ async def async_wrapper(*args, **kwargs): raise finally: tracer.end_trace(trace_id) - + @wraps(func) def sync_wrapper(*args, **kwargs): trace_id = tracer.start_trace(operation_name) start_time = time.time() - + try: result = func(*args, **kwargs) duration_ms = (time.time() - start_time) * 1000 - + tracer.add_span(trace_id, operation_name, duration_ms, {"status": "success"}) metrics.histogram(f"{operation_name}.duration_ms", duration_ms) metrics.increment(f"{operation_name}.success") - + return result except Exception as e: duration_ms = (time.time() - start_time) * 1000 @@ -261,14 +261,14 @@ def sync_wrapper(*args, **kwargs): raise finally: tracer.end_trace(trace_id) - + # Return appropriate wrapper based on function type import asyncio if asyncio.iscoroutinefunction(func): return async_wrapper else: return sync_wrapper - + return decorator @@ -279,6 +279,6 @@ def log_agent_event(logger: logging.Logger, event_type: str, agent_name: str, ** "event_type": event_type, **kwargs } - + message = f"Agent event: {event_type} - {agent_name}" logger.info(message, extra=extra) diff --git a/profiler_agent/paths.py b/profiler_agent/paths.py index d719041..021215b 100644 --- a/profiler_agent/paths.py +++ b/profiler_agent/paths.py @@ -1,5 +1,4 @@ """Path configuration for input and output directories.""" -import os from pathlib import Path @@ -28,10 +27,10 @@ def ensure_directories(): def get_input_path(filename: str) -> Path: """ Get full path for an input file. - + Args: filename: Name of the input file - + Returns: Path object for the input file """ @@ -42,39 +41,39 @@ def get_input_path(filename: str) -> Path: def get_output_path(filename: str, subfolder: str = "") -> Path: """ Get full path for an output file. - + Args: filename: Name of the output file subfolder: Optional subfolder (charts, logs, reports) - + Returns: Path object for the output file """ ensure_directories() - + if subfolder: base_dir = OUTPUT_DIR / subfolder base_dir.mkdir(parents=True, exist_ok=True) return base_dir / filename - + return OUTPUT_DIR / filename def list_input_files(extension: str = ".pdf") -> list: """ List all files in the input directory with given extension. - + Args: extension: File extension to filter (default: .pdf) - + Returns: List of Path objects for matching files """ ensure_directories() - + if not extension.startswith("."): extension = f".{extension}" - + return list(INPUT_DIR.glob(f"*{extension}")) diff --git a/profiler_agent/sub_agents/__pycache__/__init__.cpython-312.pyc b/profiler_agent/sub_agents/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 3112a95..0000000 Binary files a/profiler_agent/sub_agents/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/sub_agents/__pycache__/strategist.cpython-312.pyc b/profiler_agent/sub_agents/__pycache__/strategist.cpython-312.pyc deleted file mode 100644 index 462cb64..0000000 Binary files a/profiler_agent/sub_agents/__pycache__/strategist.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/sub_agents/__pycache__/taxonomist.cpython-312.pyc b/profiler_agent/sub_agents/__pycache__/taxonomist.cpython-312.pyc deleted file mode 100644 index 500bf5e..0000000 Binary files a/profiler_agent/sub_agents/__pycache__/taxonomist.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/sub_agents/__pycache__/trend_spotter.cpython-312.pyc b/profiler_agent/sub_agents/__pycache__/trend_spotter.cpython-312.pyc deleted file mode 100644 index 32bb210..0000000 Binary files a/profiler_agent/sub_agents/__pycache__/trend_spotter.cpython-312.pyc and /dev/null differ diff --git a/profiler_agent/sub_agents/strategist.py b/profiler_agent/sub_agents/strategist.py index d99fb15..c0bb0a3 100644 --- a/profiler_agent/sub_agents/strategist.py +++ b/profiler_agent/sub_agents/strategist.py @@ -5,6 +5,9 @@ model=config.analyzer_model, name="strategist", description="Generates actionable study plans.", - instruction="Based on the Trend Report, tell the student EXACTLY what to study. Create a Hit List, Safe Zone, and Drop List.", + instruction=( + "Based on the Trend Report, tell the student EXACTLY what to study. " + "Create a Hit List, Safe Zone, and Drop List." + ), output_key="final_study_plan" ) diff --git a/profiler_agent/sub_agents/taxonomist.py b/profiler_agent/sub_agents/taxonomist.py index 7766122..0f63ef3 100644 --- a/profiler_agent/sub_agents/taxonomist.py +++ b/profiler_agent/sub_agents/taxonomist.py @@ -6,7 +6,10 @@ model=config.classifier_model, name="taxonomist", description="Classifies educational questions by topic and cognitive difficulty.", - instruction="For every exam question provided, output tags for 'Topic' and 'Blooms Level' (Remember, Understand, Apply, Analyze). Do NOT answer the question.", + instruction=( + "For every exam question provided, output tags for 'Topic' and 'Blooms Level' " + "(Remember, Understand, Apply, Analyze). Do NOT answer the question." + ), output_key="tagged_questions", - after_agent_callback=suppress_output_callback + after_agent_callback=suppress_output_callback ) diff --git a/profiler_agent/tools.py b/profiler_agent/tools.py index 4949f88..218b83f 100644 --- a/profiler_agent/tools.py +++ b/profiler_agent/tools.py @@ -1,7 +1,7 @@ """Custom tools for the Professor Profiler agent.""" import os import json -from typing import Dict, List, Any +from typing import List from pathlib import Path from collections import Counter from .paths import get_input_path, get_output_path, list_input_files @@ -18,25 +18,20 @@ except ImportError: plt = None -try: - import pandas as pd -except ImportError: - pd = None - def read_pdf_content(file_path: str) -> dict: """ Extract text content from a PDF file. - + Args: file_path: Path to the PDF file (relative to input/ folder or absolute path) - + Returns: Dictionary with filename and content, or error message """ if PdfReader is None: return {"error": "pypdf library is not installed."} - + # If path is relative (no directory separator), look in input/ folder path = Path(file_path) if not path.is_absolute() and not os.path.exists(file_path): @@ -48,19 +43,19 @@ def read_pdf_content(file_path: str) -> dict: return { "error": f"File not found: {file_path}. Please place exam PDFs in the 'input/' folder." } - + if not os.path.exists(file_path): return {"error": f"File not found: {file_path}"} - + try: reader = PdfReader(file_path) text = "" page_count = len(reader.pages) - + for page_num, page in enumerate(reader.pages, 1): page_text = page.extract_text() text += f"\n--- Page {page_num} ---\n{page_text}" - + return { "filename": os.path.basename(file_path), "content": text, @@ -74,10 +69,10 @@ def read_pdf_content(file_path: str) -> dict: def analyze_statistics(questions_data: str) -> dict: """ Analyze statistical patterns in exam questions. - + Args: questions_data: JSON string or dict containing tagged questions - + Returns: Statistical analysis including frequency distributions """ @@ -87,27 +82,27 @@ def analyze_statistics(questions_data: str) -> dict: data = json.loads(questions_data) else: data = questions_data - + # Extract topics and bloom levels topics = [] bloom_levels = [] - + if isinstance(data, dict): questions = data.get("questions", []) elif isinstance(data, list): questions = data else: return {"error": "Invalid input format"} - + for q in questions: if isinstance(q, dict): topics.append(q.get("topic", "Unknown")) bloom_levels.append(q.get("bloom_level", "Unknown")) - + # Calculate statistics topic_freq = Counter(topics) bloom_freq = Counter(bloom_levels) - + return { "total_questions": len(questions), "topic_distribution": dict(topic_freq), @@ -135,37 +130,37 @@ def visualize_trends( ) -> dict: """ Create visualizations for exam trends. - + Args: statistics: JSON string containing statistical data output_path: Path to save the chart (saved to output/charts/ by default) chart_type: Type of chart ('bar', 'pie', 'line') - + Returns: Dictionary with chart path and metadata """ if plt is None: return {"error": "matplotlib library is not installed"} - + try: # Use output/charts directory if only filename provided if not os.path.dirname(output_path): output_path = str(get_output_path(output_path, "charts")) - + # Parse statistics if isinstance(statistics, str): stats = json.loads(statistics) else: stats = statistics - + # Create figure with subplots fig, axes = plt.subplots(1, 2, figsize=(14, 6)) - + # Plot 1: Topic Distribution if "topic_distribution" in stats: topics = list(stats["topic_distribution"].keys()) counts = list(stats["topic_distribution"].values()) - + if chart_type == "bar": axes[0].bar(topics, counts, color='skyblue') axes[0].set_xlabel('Topics') @@ -175,34 +170,34 @@ def visualize_trends( elif chart_type == "pie": axes[0].pie(counts, labels=topics, autopct='%1.1f%%') axes[0].set_title('Topic Distribution') - + # Plot 2: Bloom's Taxonomy Distribution if "bloom_distribution" in stats: blooms = list(stats["bloom_distribution"].keys()) bloom_counts = list(stats["bloom_distribution"].values()) - + axes[1].bar(blooms, bloom_counts, color='lightcoral') axes[1].set_xlabel('Bloom\'s Level') axes[1].set_ylabel('Frequency') axes[1].set_title('Cognitive Complexity Distribution') axes[1].tick_params(axis='x', rotation=45) - + plt.tight_layout() - + # Ensure output directory exists os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else ".", exist_ok=True) - + # Save figure plt.savefig(output_path, dpi=150, bbox_inches='tight') plt.close() - + return { "chart_path": output_path, "chart_type": chart_type, "success": True, "message": f"Chart saved to {output_path}" } - + except Exception as e: return {"error": f"Failed to create visualization: {str(e)}"} @@ -210,18 +205,18 @@ def visualize_trends( def compare_exams(exam_files: List[str]) -> dict: """ Compare multiple exam papers to identify trends over time. - + Args: exam_files: List of PDF file paths to compare - + Returns: Comparison analysis with trends """ if not exam_files: return {"error": "No exam files provided"} - + results = [] - + for file_path in exam_files: content = read_pdf_content(file_path) if "error" not in content: @@ -230,7 +225,7 @@ def compare_exams(exam_files: List[str]) -> dict: "page_count": content.get("page_count", 0), "content_length": len(content.get("content", "")) }) - + return { "total_exams": len(results), "exams_analyzed": results, @@ -241,18 +236,18 @@ def compare_exams(exam_files: List[str]) -> dict: def list_available_exams() -> dict: """ List all PDF files available in the input directory. - + Returns: Dictionary with list of available exam files """ try: pdf_files = list_input_files(".pdf") - + return { "count": len(pdf_files), "files": [f.name for f in pdf_files], "paths": [str(f) for f in pdf_files], - "message": f"Found {len(pdf_files)} PDF file(s) in input/ directory" + "message": "Found {count} PDF file(s) in input/ directory".format(count=len(pdf_files)) } except Exception as e: - return {"error": f"Failed to list files: {str(e)}"} \ No newline at end of file + return {"error": "Failed to list files: {error}".format(error=str(e))} diff --git a/tests/test_agent.py b/tests/test_agent.py index f2d18e4..097b0db 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -6,6 +6,8 @@ import sys from pathlib import Path +import pytest + # Ensure repo root is on sys.path so running this script directly # (python tests/test_agent.py) can import local packages like `google.adk`. repo_root = Path(__file__).resolve().parent.parent @@ -19,38 +21,40 @@ from google.genai import types as genai_types +@pytest.mark.asyncio async def test_agent_initialization(): """Test agent initialization.""" print("TEST 1: Agent Initialization") print("-" * 60) - + assert root_agent.name == "professor_profiler_agent" assert len(root_agent.sub_agents) == 3 assert len(root_agent.tools) > 0 - + print(f"✅ Root agent: {root_agent.name}") - print(f"✅ Sub-agents: {[a.name for a in root_agent.sub_agents]}") - print(f"✅ Tools: {[t.name for t in root_agent.tools]}") + print("✅ Sub-agents: {sub_agents}".format(sub_agents=[a.name for a in root_agent.sub_agents])) + print("✅ Tools: {tools}".format(tools=[t.name for t in root_agent.tools])) print() +@pytest.mark.asyncio async def test_session_service(): """Test session service.""" print("TEST 2: Session Service") print("-" * 60) - + session_service = InMemorySessionService() - + # Create session session = await session_service.create_session( app_name="test_app", user_id="test_user", session_id="test_session" ) - + assert session["session_id"] == "test_session" - print(f"✅ Created session: {session['session_id']}") - + print("✅ Created session: {session_id}".format(session_id=session['session_id'])) + # Add messages await session_service.add_message( app_name="test_app", @@ -59,16 +63,16 @@ async def test_session_service(): role="user", content="Test message" ) - + messages = await session_service.get_messages( app_name="test_app", user_id="test_user", session_id="test_session" ) - + assert len(messages) == 1 - print(f"✅ Added and retrieved message") - + print("✅ Added and retrieved message") + # Update context await session_service.update_context( app_name="test_app", @@ -76,37 +80,38 @@ async def test_session_service(): session_id="test_session", context_updates={"test_key": "test_value"} ) - + context = await session_service.get_context( app_name="test_app", user_id="test_user", session_id="test_session" ) - + assert context["test_key"] == "test_value" - print(f"✅ Updated and retrieved context") + print("✅ Updated and retrieved context") print() +@pytest.mark.asyncio async def test_tools(): """Test custom tools.""" print("TEST 3: Custom Tools") print("-" * 60) - + from profiler_agent.tools import read_pdf_content, analyze_statistics import json - + # Ensure mock file exists os.makedirs("tests/sample_data", exist_ok=True) test_file = "tests/sample_data/test.pdf" with open(test_file, "w") as f: f.write("mock pdf content") - + # Test PDF tool result = read_pdf_content(test_file) assert "filename" in result or "error" in result - print(f"✅ PDF tool executed: {result.get('filename', 'error')}") - + print("✅ PDF tool executed: {filename}".format(filename=result.get('filename', 'error'))) + # Test statistics tool mock_data = { "questions": [ @@ -114,19 +119,20 @@ async def test_tools(): {"topic": "Math", "bloom_level": "Analyze"} ] } - + stats = analyze_statistics(json.dumps(mock_data)) assert "total_questions" in stats assert stats["total_questions"] == 2 - print(f"✅ Statistics tool executed: {stats['total_questions']} questions") + print("✅ Statistics tool executed: {count} questions".format(count=stats['total_questions'])) print() +@pytest.mark.asyncio async def test_runner_execution(): """Test runner with agent execution.""" print("TEST 4: Runner Execution") print("-" * 60) - + # Setup setup_logging(level="INFO") session_service = InMemorySessionService() @@ -135,21 +141,21 @@ async def test_runner_execution(): user_id="test_user", session_id="test_sess" ) - + runner = Runner( agent=root_agent, app_name="test_app", session_service=session_service ) - + # Ensure mock file exists os.makedirs("tests/sample_data", exist_ok=True) with open("tests/sample_data/physics_2024.pdf", "w") as f: f.write("mock content") - + query = "Analyze tests/sample_data/physics_2024.pdf" - print(f"Query: {query}") - + print("Query: {query}".format(query=query)) + final_response = None async for event in runner.run_async( user_id="test_user", @@ -161,22 +167,23 @@ async def test_runner_execution(): ): if event.is_final_response(): final_response = event.content.parts[0].text - print(f"Response received: {final_response[:100]}...") - + print("Response received: {response}...".format(response=final_response[:100])) + assert final_response is not None - print(f"✅ Runner executed successfully") + print("✅ Runner executed successfully") print() +@pytest.mark.asyncio async def test_memory_bank(): """Test memory bank functionality.""" print("TEST 5: Memory Bank") print("-" * 60) - + from profiler_agent.memory import MemoryBank - + memory_bank = MemoryBank(storage_path="test_memory.json") - + # Add memory memory_id = memory_bank.add_memory( user_id="test_user", @@ -184,19 +191,19 @@ async def test_memory_bank(): content={"key": "value"}, tags=["test"] ) - - print(f"✅ Added memory: {memory_id}") - + + print("✅ Added memory: {memory_id}".format(memory_id=memory_id)) + # Retrieve memory memories = memory_bank.get_memories("test_user") assert len(memories) > 0 - print(f"✅ Retrieved {len(memories)} memories") - + print("✅ Retrieved {count} memories".format(count=len(memories))) + # Search results = memory_bank.search_memories("test_user", "value") assert len(results) > 0 - print(f"✅ Search found {len(results)} results") - + print("✅ Search found {count} results".format(count=len(results))) + # Cleanup if os.path.exists("test_memory.json"): os.remove("test_memory.json") @@ -209,31 +216,31 @@ async def main(): print("PROFESSOR PROFILER - INTEGRATION TESTS") print("="*60) print() - + api_key = os.getenv("GOOGLE_API_KEY") if not api_key: print("⚠️ GOOGLE_API_KEY not set - using mock responses") else: - print(f"✅ GOOGLE_API_KEY configured") - + print("✅ GOOGLE_API_KEY configured") + print() - + try: await test_agent_initialization() await test_session_service() await test_tools() await test_memory_bank() await test_runner_execution() - + print("="*60) print("✅ ALL TESTS PASSED") print("="*60) - + except AssertionError as e: - print(f"\n❌ TEST FAILED: {e}") + print("\n❌ TEST FAILED: {error}".format(error=e)) raise except Exception as e: - print(f"\n❌ ERROR: {e}") + print("\n❌ ERROR: {error}".format(error=e)) import traceback traceback.print_exc() raise