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