Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions tourist_scheduling_system/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ dependencies = [
"httpx>=0.28.1",
# Web framework
"uvicorn>=0.40.0",
"fastapi>=0.115.0,<0.124.0",
"fastapi>=0.115.0,<0.129.0",
"websockets>=11.0.0",
# AI/LLM
"openai>=2.15.0",
"google-adk>=1.22.1",
"google-adk==1.23.0",
"litellm>=1.80.16",
# Configuration
"python-dotenv>=1.0.0",
Expand Down
15 changes: 13 additions & 2 deletions tourist_scheduling_system/src/agents/guide_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ async def create_guide_agent(
from google.adk.agents.llm_agent import LlmAgent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.tools import load_memory

transport_mode = get_transport_mode()
logger.info(f"[Guide {guide_id}] Creating agent with transport mode: {transport_mode}")
Expand Down Expand Up @@ -135,6 +136,7 @@ async def create_guide_agent(

Be helpful and professional in describing your tour offerings.""",
sub_agents=[scheduler_remote],
tools=[load_memory],
)

return guide_agent
Expand Down Expand Up @@ -162,7 +164,10 @@ async def run_guide_agent(
max_group_size: Maximum tourists per tour
"""
# Import ADK runner at runtime
from google.adk.runners import InMemoryRunner
from google.adk.runners import Runner
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.sessions import DatabaseSessionService
from src.core.memory import FileMemoryService

transport_mode = get_transport_mode()
print(f"[Guide {guide_id}] Starting with ADK (transport: {transport_mode})...")
Expand Down Expand Up @@ -205,7 +210,13 @@ async def run_guide_agent(

# Create the guide agent
agent = await create_guide_agent(guide_id, scheduler_url, a2a_client_factory)
runner = InMemoryRunner(agent=agent)
runner = Runner(
agent=agent,
app_name=f"guide_{guide_id}",
artifact_service=InMemoryArtifactService(),
session_service=DatabaseSessionService(db_url=f"sqlite+aiosqlite:///guide_sessions_{guide_id}.db"),
memory_service=FileMemoryService(f"guide_memory_{guide_id}.json")
)

# Create offer message
message = create_guide_offer_message(
Expand Down
45 changes: 41 additions & 4 deletions tourist_scheduling_system/src/agents/scheduler_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def get_scheduler_agent():
# Import ADK components at runtime
from google.adk.agents.llm_agent import LlmAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.tools import load_memory

# Get model configuration from environment
from core.model_factory import create_llm_model
Expand Down Expand Up @@ -127,6 +128,7 @@ def get_scheduler_agent():
run_scheduling,
get_schedule_status,
clear_scheduler_state,
load_memory,
],
)

Expand Down Expand Up @@ -157,18 +159,33 @@ def create_scheduler_app(host: str = "localhost", port: int = 10000):
"""
# Import ADK components at runtime
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from google.adk.runners import Runner
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.sessions import DatabaseSessionService
from src.core.memory import FileMemoryService

# Load agent card from a2a_cards directory
from src.core.a2a_cards import get_scheduler_card
agent_card = get_scheduler_card(host=host, port=port)
logger.info(f"[ADK Scheduler] Using agent card: {agent_card.name} v{agent_card.version}")

agent = get_scheduler_agent()

runner = Runner(
agent=agent,
app_name="scheduler_agent",
artifact_service=InMemoryArtifactService(),
session_service=DatabaseSessionService(db_url="sqlite+aiosqlite:///scheduler_sessions.db"),
memory_service=FileMemoryService("scheduler_memory.json")
)

return to_a2a(
get_scheduler_agent(),
agent,
host=host,
port=port,
protocol="http",
agent_card=agent_card,
runner=runner,
)


Expand Down Expand Up @@ -199,7 +216,18 @@ def create_scheduler_a2a_components(host: str = "localhost", port: int = 10000):
agent = get_scheduler_agent()

# Create runner for the agent
runner = InMemoryRunner(agent=agent)
from google.adk.runners import Runner
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.sessions import DatabaseSessionService
from src.core.memory import FileMemoryService

runner = Runner(
agent=agent,
app_name="scheduler_agent",
artifact_service=InMemoryArtifactService(),
session_service=DatabaseSessionService(db_url="sqlite+aiosqlite:///scheduler_sessions.db"),
memory_service=FileMemoryService("scheduler_memory.json")
)

# Create A2A executor wrapping the ADK runner
agent_executor = A2aAgentExecutor(runner=runner)
Expand All @@ -216,13 +244,22 @@ def create_scheduler_a2a_components(host: str = "localhost", port: int = 10000):
async def run_console_demo():
"""Run a console demo of the scheduler agent."""
# Import ADK runner at runtime
from google.adk.runners import InMemoryRunner
from google.adk.runners import Runner
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.sessions import DatabaseSessionService
from src.core.memory import FileMemoryService

print("=" * 60)
print("ADK Scheduler Agent - Console Demo")
print("=" * 60)

runner = InMemoryRunner(agent=get_scheduler_agent())
runner = Runner(
agent=get_scheduler_agent(),
app_name="scheduler_console_demo",
artifact_service=InMemoryArtifactService(),
session_service=DatabaseSessionService(db_url="sqlite+aiosqlite:///scheduler_demo_sessions.db"),
memory_service=FileMemoryService("scheduler_demo_memory.json")
)

# Demo messages
demo_messages = [
Expand Down
15 changes: 13 additions & 2 deletions tourist_scheduling_system/src/agents/tourist_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ async def create_tourist_agent(
from google.adk.agents.llm_agent import LlmAgent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.tools import load_memory

transport_mode = get_transport_mode()
logger.info(f"[Tourist {tourist_id}] Creating agent with transport mode: {transport_mode}")
Expand Down Expand Up @@ -129,6 +130,7 @@ async def create_tourist_agent(
After sending your request, you should receive a schedule proposal with matched guides.
Be polite and clear in describing what kind of tour experience you're looking for.""",
sub_agents=[scheduler_remote],
tools=[load_memory],
)

return tourist_agent
Expand All @@ -154,7 +156,10 @@ async def run_tourist_agent(
budget: Maximum hourly budget in dollars
"""
# Import ADK runner at runtime
from google.adk.runners import InMemoryRunner
from google.adk.runners import Runner
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.sessions import DatabaseSessionService
from src.core.memory import FileMemoryService

transport_mode = get_transport_mode()
print(f"[Tourist {tourist_id}] Starting with ADK (transport: {transport_mode})...")
Expand Down Expand Up @@ -197,7 +202,13 @@ async def run_tourist_agent(

# Create the tourist agent
agent = await create_tourist_agent(tourist_id, scheduler_url, a2a_client_factory)
runner = InMemoryRunner(agent=agent)
runner = Runner(
agent=agent,
app_name=f"tourist_{tourist_id}",
artifact_service=InMemoryArtifactService(),
session_service=DatabaseSessionService(db_url=f"sqlite+aiosqlite:///tourist_sessions_{tourist_id}.db"),
memory_service=FileMemoryService(f"tourist_memory_{tourist_id}.json")
)

# Create request message
message = create_tourist_request_message(
Expand Down
89 changes: 89 additions & 0 deletions tourist_scheduling_system/src/core/memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import json
import os
import threading
from typing import Any, Dict, List

from google.adk.memory.base_memory_service import BaseMemoryService, SearchMemoryResponse
from google.adk.memory.memory_entry import MemoryEntry
from google.adk.sessions.session import Session
from google.adk.events.event import Event
from google.adk.memory import _utils

class FileMemoryService(BaseMemoryService):
"""A persistent file-based memory service."""

def __init__(self, file_path: str = "agent_memory.json"):
self.file_path = file_path
self._lock = threading.Lock()
self._ensure_file()

def _ensure_file(self):
if not os.path.exists(self.file_path):
with open(self.file_path, 'w') as f:
json.dump({}, f)

def _load_events(self) -> Dict[str, Dict[str, List[Dict]]]:
try:
with open(self.file_path, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}

def _save_events(self, data: Dict[str, Dict[str, List[Dict]]]):
with open(self.file_path, 'w') as f:
json.dump(data, f, indent=2)

def _user_key(self, app_name: str, user_id: str):
return f'{app_name}/{user_id}'

async def add_session_to_memory(self, session: Session):
user_key = self._user_key(session.app_name, session.user_id)

# Serialize events
serialized_events = []
for event in session.events:
if event.content and event.content.parts:
# Use model_dump(mode='json') for serialization friendly dict
serialized_events.append(event.model_dump(mode='json'))

with self._lock:
data = self._load_events()
if user_key not in data:
data[user_key] = {}
data[user_key][session.id] = serialized_events
self._save_events(data)

async def search_memory(self, *, app_name: str, user_id: str, query: str) -> SearchMemoryResponse:
user_key = self._user_key(app_name, user_id)

with self._lock:
data = self._load_events()
user_sessions = data.get(user_key, {})

response = SearchMemoryResponse()

# Simple keyword search (same as InMemoryMemoryService)
query_words = set(query.lower().split())

for session_id, events_data in user_sessions.items():
for event_dict in events_data:
try:
event = Event.model_validate(event_dict)
except Exception:
continue

if not event.content or not event.content.parts:
continue

text = ' '.join([p.text for p in event.content.parts if p.text])
text_lower = text.lower()

if any(w in text_lower for w in query_words):
response.memories.append(
MemoryEntry(
content=event.content,
author=event.author,
timestamp=_utils.format_timestamp(event.timestamp)
)
)
return response
113 changes: 113 additions & 0 deletions tourist_scheduling_system/tests/test_agent_memory_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@

import pytest
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch, AsyncMock

# Check if ADK is available
try:
from google.adk.agents.llm_agent import LlmAgent
ADK_AVAILABLE = True
except ImportError:
ADK_AVAILABLE = False

@pytest.mark.skipif(not ADK_AVAILABLE, reason="ADK not installed")
class TestAgentMemoryConfiguration:

def test_scheduler_uses_file_memory(self):
from src.agents.scheduler_agent import create_scheduler_app
from src.core.memory import FileMemoryService
from google.adk.sessions import DatabaseSessionService
from google.adk.runners import Runner

# Patch where it is defined since it is imported inside the function
with patch("google.adk.a2a.utils.agent_to_a2a.to_a2a") as mock_to_a2a:
create_scheduler_app()

# Check if to_a2a was called
assert mock_to_a2a.called

# Get the arguments passed to to_a2a
call_args = mock_to_a2a.call_args
kwargs = call_args.kwargs

# Check if runner was passed
if "runner" in kwargs and kwargs["runner"] is not None:
runner = kwargs["runner"]
assert isinstance(runner.memory_service, FileMemoryService)
assert runner.memory_service.file_path == "scheduler_memory.json"
assert isinstance(runner.session_service, DatabaseSessionService)
else:
pytest.fail("runner argument not passed to to_a2a")

@pytest.mark.asyncio
async def test_guide_uses_file_memory(self):
from src.agents.guide_agent import run_guide_agent

# Mock dependencies
with patch("google.adk.runners.Runner") as MockRunner, \
patch("src.core.memory.FileMemoryService") as MockFileMemoryService, \
patch("google.adk.sessions.DatabaseSessionService") as MockDatabaseSessionService, \
patch("src.agents.guide_agent.create_guide_agent", new_callable=AsyncMock) as mock_create_agent:

mock_create_agent.return_value = MagicMock()
mock_runner_instance = MockRunner.return_value
mock_runner_instance.run_debug = AsyncMock(return_value=[])

await run_guide_agent(
guide_id="g1",
scheduler_url="http://localhost:8000",
categories="History",
available_start="2025-06-01T09:00:00",
available_end="2025-06-01T17:00:00",
hourly_rate=50.0
)

# Check FileMemoryService usage
MockFileMemoryService.assert_called_once_with("guide_memory_g1.json")

# Check DatabaseSessionService usage
MockDatabaseSessionService.assert_called_once_with(db_url="sqlite+aiosqlite:///guide_sessions_g1.db")

# Check Runner usage
_, kwargs = MockRunner.call_args
assert "memory_service" in kwargs
assert kwargs["memory_service"] == MockFileMemoryService.return_value
assert "session_service" in kwargs
assert kwargs["session_service"] == MockDatabaseSessionService.return_value

@pytest.mark.asyncio
async def test_tourist_uses_file_memory(self):
from src.agents.tourist_agent import run_tourist_agent

# Mock dependencies
with patch("google.adk.runners.Runner") as MockRunner, \
patch("src.core.memory.FileMemoryService") as MockFileMemoryService, \
patch("google.adk.sessions.DatabaseSessionService") as MockDatabaseSessionService, \
patch("src.agents.tourist_agent.create_tourist_agent", new_callable=AsyncMock) as mock_create_agent:

mock_create_agent.return_value = MagicMock()
mock_runner_instance = MockRunner.return_value
mock_runner_instance.run_debug = AsyncMock(return_value=[])

await run_tourist_agent(
tourist_id="t1",
scheduler_url="http://localhost:8000",
availability_start="2025-06-01T09:00:00",
availability_end="2025-06-01T17:00:00",
preferences="History",
budget=100.0
)

# Check FileMemoryService usage
MockFileMemoryService.assert_called_once_with("tourist_memory_t1.json")

# Check DatabaseSessionService usage
MockDatabaseSessionService.assert_called_once_with(db_url="sqlite+aiosqlite:///tourist_sessions_t1.db")

# Check Runner usage
_, kwargs = MockRunner.call_args
assert "memory_service" in kwargs
assert kwargs["memory_service"] == MockFileMemoryService.return_value
assert "session_service" in kwargs
assert kwargs["session_service"] == MockDatabaseSessionService.return_value
Loading