Skip to content

Commit d5fcf91

Browse files
Copilotzlendo1
andcommitted
Refactor AI service to use single endpoint for itinerary and leg enrichment
Co-authored-by: zlendo1 <115471708+zlendo1@users.noreply.github.com>
1 parent d81b874 commit d5fcf91

4 files changed

Lines changed: 79 additions & 252 deletions

File tree

app/api/v1/endpoints/routes.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,13 @@ async def search_routes(request: RouteSearchRequest) -> Any:
6161
first=request.num_itineraries,
6262
)
6363

64-
# Enhance each leg with AI insights (with graceful degradation)
64+
# Enhance each itinerary with AI insights (with graceful degradation)
6565
for itinerary in itineraries:
66-
for leg in itinerary.legs:
67-
try:
68-
ai_insight = await ai_agents_service.get_leg_insight(leg)
69-
leg.ai_insight = ai_insight
70-
except Exception as e: # pylint: disable=broad-except
71-
# Gracefully degrade - log warning but continue without AI insight
72-
logger.warning("Failed to get AI insight for leg: %s", str(e))
73-
leg.ai_insight = None
74-
75-
# Get AI description for the overall itinerary
7666
try:
77-
ai_description = await ai_agents_service.get_itinerary_insight(itinerary)
78-
itinerary.ai_description = ai_description
67+
await ai_agents_service.get_itinerary_insight(itinerary)
7968
except Exception as e: # pylint: disable=broad-except
80-
# Gracefully degrade - log warning but continue without AI description
81-
logger.warning("Failed to get AI description for itinerary: %s", str(e))
82-
itinerary.ai_description = None
69+
# Gracefully degrade - log warning but continue without AI insights
70+
logger.warning("Failed to get AI insights for itinerary: %s", str(e))
8371

8472
logger.info("Route search successful: found %d itineraries", len(itineraries))
8573

app/services/ai_agents_service.py

Lines changed: 25 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -65,66 +65,26 @@ async def health_check(self) -> ServiceHealth:
6565
message=f"AI-agents API check failed: {str(e)}",
6666
)
6767

68-
async def get_leg_insight(self, leg: Leg) -> Optional[str]:
68+
async def get_itinerary_insight(self, itinerary: Itinerary) -> None:
6969
"""
70-
Get AI-generated insight for a specific leg of the journey.
71-
72-
Args:
73-
leg: The leg object containing journey segment information
74-
75-
Returns:
76-
Optional[str]: AI-generated insight text, or None if unavailable
77-
78-
Raises:
79-
No exceptions - gracefully degrades by returning None on errors
80-
"""
81-
try:
82-
client = self._get_client()
83-
84-
# Prepare request payload with leg information
85-
payload: dict[str, str | int | float | dict[str, str] | None] = {
86-
"mode": leg.mode.value,
87-
"duration": leg.duration,
88-
"distance": leg.distance,
89-
"from_place": leg.from_place.name,
90-
"to_place": leg.to_place.name,
91-
}
92-
93-
# Include route information if available (e.g., bus number)
94-
if leg.route:
95-
payload["route"] = {
96-
"short_name": leg.route.short_name,
97-
"long_name": leg.route.long_name,
98-
}
99-
100-
response = await client.post("/api/v1/insight/leg", json=payload)
101-
102-
if response.status_code == 200:
103-
data = response.json()
104-
return data.get("insight")
105-
106-
logger.warning("AI agents service returned non-200 status: %s", response.status_code)
107-
return None
108-
109-
except httpx.TimeoutException:
110-
logger.warning("AI agents service request timed out")
111-
return None
112-
except Exception as e: # pylint: disable=broad-except
113-
logger.warning("Failed to get AI insight: %s", str(e))
114-
return None
115-
116-
async def get_itinerary_insight(self, itinerary: Itinerary) -> Optional[str]:
117-
"""
118-
Get AI-generated insight for a complete itinerary.
70+
Get AI-generated insights for a complete itinerary and enrich it with AI data.
71+
72+
This method sends the entire itinerary to the AI service and receives back:
73+
- ai_description: Overall description of the itinerary
74+
- ai_insights: List of insights for each leg
75+
76+
The method directly modifies the itinerary object in place, setting:
77+
- itinerary.ai_description
78+
- leg.ai_insight for each leg
11979
12080
Args:
12181
itinerary: The itinerary object containing the complete journey information
12282
12383
Returns:
124-
Optional[str]: AI-generated insight text for the itinerary, or None if unavailable
84+
None - modifies the itinerary object in place
12585
12686
Raises:
127-
No exceptions - gracefully degrades by returning None on errors
87+
No exceptions - gracefully degrades by returning without modification on errors
12888
"""
12989
try:
13090
client = self._get_client()
@@ -160,20 +120,29 @@ async def get_itinerary_insight(self, itinerary: Itinerary) -> Optional[str]:
160120

161121
if response.status_code == 200:
162122
data = response.json()
163-
return data.get("insight")
123+
124+
# Set the itinerary-level AI description
125+
itinerary.ai_description = data.get("ai_description")
126+
127+
# Set AI insights for each leg
128+
ai_insights = data.get("ai_insights", [])
129+
for idx, leg in enumerate(itinerary.legs):
130+
if idx < len(ai_insights):
131+
leg.ai_insight = ai_insights[idx]
132+
else:
133+
leg.ai_insight = None
134+
135+
return
164136

165137
logger.warning(
166138
"AI agents service returned non-200 status for itinerary insight: %s",
167139
response.status_code,
168140
)
169-
return None
170141

171142
except httpx.TimeoutException:
172143
logger.warning("AI agents service request timed out for itinerary insight")
173-
return None
174144
except Exception as e: # pylint: disable=broad-except
175145
logger.warning("Failed to get AI itinerary insight: %s", str(e))
176-
return None
177146

178147

179148
# Singleton instance for dependency injection

tests/endpoints/test_routes.py

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -440,18 +440,13 @@ def test_search_routes_with_ai_insights_success(client: TestClient, sample_itine
440440
with patch("app.api.v1.endpoints.routes.ai_agents_service") as mock_ai_service:
441441
mock_routing_service.get_itinaries = AsyncMock(return_value=sample_itineraries)
442442

443-
# Mock AI service to return insights for legs
444-
async def mock_get_insight(leg):
445-
if leg.mode == TransportMode.WALK:
446-
return "Short walk to the bus stop."
447-
return "Express bus with comfortable seats."
443+
# Mock AI service to enrich itinerary in place
444+
async def mock_get_itinerary_insight(itinerary):
445+
itinerary.ai_description = "This route offers a good balance of walking and public transport."
446+
itinerary.legs[0].ai_insight = "Short walk to the bus stop."
447+
itinerary.legs[1].ai_insight = "Express bus with comfortable seats."
448448

449-
mock_ai_service.get_leg_insight = AsyncMock(side_effect=mock_get_insight)
450-
451-
# Mock AI service to return itinerary description
452-
mock_ai_service.get_itinerary_insight = AsyncMock(
453-
return_value="This route offers a good balance of walking and public transport."
454-
)
449+
mock_ai_service.get_itinerary_insight = AsyncMock(side_effect=mock_get_itinerary_insight)
455450

456451
response = client.post(
457452
"/api/v1/routes/search",
@@ -475,9 +470,6 @@ async def mock_get_insight(leg):
475470
# Check second leg (BUS)
476471
assert itinerary["legs"][1]["ai_insight"] == "Express bus with comfortable seats."
477472

478-
# Verify AI service was called for each leg
479-
assert mock_ai_service.get_leg_insight.call_count == 2
480-
481473
# Verify AI description for the itinerary
482474
assert (
483475
itinerary["ai_description"]
@@ -493,9 +485,12 @@ def test_search_routes_with_ai_service_unavailable(client: TestClient, sample_it
493485
with patch("app.api.v1.endpoints.routes.ai_agents_service") as mock_ai_service:
494486
mock_routing_service.get_itinaries = AsyncMock(return_value=sample_itineraries)
495487

496-
# Mock AI service to return None (service unavailable)
497-
mock_ai_service.get_leg_insight = AsyncMock(return_value=None)
498-
mock_ai_service.get_itinerary_insight = AsyncMock(return_value=None)
488+
# Mock AI service to do nothing (service unavailable)
489+
async def mock_get_itinerary_insight_unavailable(itinerary):
490+
# Service unavailable - does not modify itinerary
491+
pass
492+
493+
mock_ai_service.get_itinerary_insight = AsyncMock(side_effect=mock_get_itinerary_insight_unavailable)
499494

500495
response = client.post(
501496
"/api/v1/routes/search",
@@ -522,22 +517,17 @@ def test_search_routes_with_ai_service_unavailable(client: TestClient, sample_it
522517

523518

524519
def test_search_routes_with_ai_service_partial_failure(client: TestClient, sample_itineraries):
525-
"""Test graceful degradation when AI service fails for some legs."""
520+
"""Test graceful degradation when AI service provides partial data."""
526521
with patch("app.api.v1.endpoints.routes.routing_service") as mock_routing_service:
527522
with patch("app.api.v1.endpoints.routes.ai_agents_service") as mock_ai_service:
528523
mock_routing_service.get_itinaries = AsyncMock(return_value=sample_itineraries)
529524

530-
# Mock AI service to succeed for first leg, fail for second
531-
call_count = 0
532-
533-
async def mock_get_insight_partial(leg):
534-
nonlocal call_count
535-
call_count += 1
536-
if call_count == 1:
537-
return "Short walk to the bus stop."
538-
return None # Simulate failure for second leg
525+
# Mock AI service to provide partial data
526+
async def mock_get_itinerary_insight_partial(itinerary):
527+
# Only set description, not leg insights
528+
itinerary.ai_description = "This is a good route."
539529

540-
mock_ai_service.get_leg_insight = AsyncMock(side_effect=mock_get_insight_partial)
530+
mock_ai_service.get_itinerary_insight = AsyncMock(side_effect=mock_get_itinerary_insight_partial)
541531

542532
response = client.post(
543533
"/api/v1/routes/search",
@@ -550,10 +540,9 @@ async def mock_get_insight_partial(leg):
550540
assert response.status_code == 200
551541
data = response.json()
552542

553-
# Verify first leg has insight
554-
assert data["itineraries"][0]["legs"][0]["ai_insight"] == "Short walk to the bus stop."
555-
556-
# Verify second leg has no insight (graceful degradation)
543+
# Verify itinerary has description but legs don't have insights
544+
assert data["itineraries"][0]["ai_description"] == "This is a good route."
545+
assert data["itineraries"][0]["legs"][0]["ai_insight"] is None
557546
assert data["itineraries"][0]["legs"][1]["ai_insight"] is None
558547

559548

@@ -564,7 +553,6 @@ def test_search_routes_with_ai_service_exception(client: TestClient, sample_itin
564553
mock_routing_service.get_itinaries = AsyncMock(return_value=sample_itineraries)
565554

566555
# Mock AI service to raise an exception
567-
mock_ai_service.get_leg_insight = AsyncMock(side_effect=Exception("AI service error"))
568556
mock_ai_service.get_itinerary_insight = AsyncMock(
569557
side_effect=Exception("AI service error")
570558
)

0 commit comments

Comments
 (0)