TransitPulse / agent.py
DeepikaChintamreddy
TransitPulse — GPU-Accelerated Reliability Engine
93e2220
Raw
History Blame Contribute Delete
8.39 kB
"""Gemini-powered natural language transit operations analyst agent.
Uses google-genai function calling to query aggregated reliability metrics.
"""
from __future__ import annotations
import os
from typing import Any
from google import genai
from google.genai import types
from config import CFG
from db import DB
# ── Define Tools for Gemini Function Calling ──────────────────────────
def get_route_score(route_id: str) -> str:
"""Returns the current reliability score and basic metrics for a specific route."""
try:
timeline = DB.get_route_timeline(route_id)
if not timeline:
return f"Error: Route {route_id} was not found in the database."
# Get the latest entry
latest = timeline[-1]
res = (
f"Route: {route_id}\n"
f"Date: {latest.get('date')}\n"
f"Reliability Score: {latest.get('reliability_score'):.2f} / 100\n"
f"Average Headway: {latest.get('mean_headway'):.1f} minutes\n"
f"Average Dwell Time: {latest.get('mean_dwell_sec'):.1f} seconds\n"
f"Bunching Events Count: {latest.get('bunching_count')}\n"
f"Gap Events Count: {latest.get('gap_count')}"
)
return res
except Exception as e:
return f"Error querying route score: {str(e)}"
def get_worst_segments(limit: int = 5) -> str:
"""Returns a list of the N worst performing route segments that need schedule intervention."""
try:
worst = DB.get_worst_segments(limit=limit)
if not worst:
return "No segment metrics available in the database."
lines = ["Worst Performing Route Segments:"]
for idx, item in enumerate(worst):
lines.append(
f"{idx+1}. Route {item['route_id']}, Segment {item['stop_id']}: "
f"Reliability Score: {item['reliability_score']:.2f}, "
f"Trips: {item['total_trips']}, Bunching Rate: {item['bunching_rate']*100:.1f}%, "
f"Gap Rate: {item['gap_rate']*100:.1f}%"
)
return "\n".join(lines)
except Exception as e:
return f"Error querying worst segments: {str(e)}"
def compare_periods(route_id: str, date1: str, date2: str) -> str:
"""Compares route performance metrics between two dates."""
try:
metrics = DB.compare_periods(route_id, date1, date2)
if len(metrics) < 2:
return f"Compare error: Could not find data for route {route_id} on both dates {date1} and {date2}."
res = f"Comparison of Route {route_id} between {date1} and {date2}:\n"
for item in metrics:
res += (
f"- Date: {item.get('date', 'N/A')}: "
f"Reliability: {item.get('reliability_score', 0):.2f}, "
f"Headway: {item.get('mean_headway', 0):.1f} min, "
f"Bunching: {item.get('bunching_count', 0)}, "
f"Gaps: {item.get('gap_count', 0)}\n"
)
return res
except Exception as e:
return f"Error comparing periods: {str(e)}"
def explain_anomaly(anomaly_id_or_route: str) -> str:
"""Explains recent anomalies for a route or segment."""
try:
anomalies = DB.get_anomalies(route_id=anomaly_id_or_route, limit=5)
if not anomalies:
return f"No recent anomalies recorded for {anomaly_id_or_route}."
lines = [f"Recent anomaly events for {anomaly_id_or_route}:"]
for idx, a in enumerate(anomalies):
lines.append(
f"- Time: {a['timestamp_str']} | Vehicle: {a['vehicle_id']} | "
f"Stop: {a['stop_id']} (Seq {a['stop_sequence']}) | "
f"Type: {a['anomaly_type'].upper()} (Headway: {a['headway_min']:.1f} min vs Scheduled: {a['scheduled_headway_min']:.1f} min)"
)
return "\n".join(lines)
except Exception as e:
return f"Error explaining anomalies: {str(e)}"
# Map of available function names to local python callables
TOOLS_MAP = {
"get_route_score": get_route_score,
"get_worst_segments": get_worst_segments,
"compare_periods": compare_periods,
"explain_anomaly": explain_anomaly
}
def ask_gemini(question: str) -> str:
"""Runs a function-calling session with Gemini using the google-genai SDK."""
api_key = CFG.gemini_api_key
if not api_key:
api_key = os.environ.get("GEMINI_API_KEY", "")
if not api_key:
return (
"Gemini Agent is offline because the GEMINI_API_KEY environment variable is not set. "
"Please configure the key to enable operations intelligence chat."
)
# Initialize google-genai client
client = genai.Client(api_key=api_key)
system_instruction = (
"You are TransitPulse's AI Lead Transit Operations Analyst. "
"Your role is to help a transit depot manager decide which routes and segments need schedule "
"intervention this week. "
"You have direct access to database querying tools. You must adhere to the following rules:\n"
"1. GROUND ALL CLAIMS IN DATA. Never state a route or segment performance number without querying it first.\n"
"2. NEVER HALLUCINATE route IDs, segment IDs, or scores. If a route isn't returned, state it's not present.\n"
"3. Keep answers clear, structured, and quantitative.\n"
"4. ALWAYS end your response with a concrete recommended action (e.g., 'Recommendation: Recalibrate route DTC-015 scheduled headway')."
)
# List of functions provided to Gemini
tools_declarations = [
get_route_score,
get_worst_segments,
compare_periods,
explain_anomaly
]
try:
# Step 1: Initial call to Gemini with the user's question
response = client.models.generate_content(
model=CFG.gemini_model,
contents=question,
config=types.GenerateContentConfig(
system_instruction=system_instruction,
tools=tools_declarations
)
)
# Step 2: Handle function calls if Gemini requests them
# Note: In a production loop, there could be multiple turns of function calling.
# We will handle up to 3 function execution cycles.
contents = [question]
for _ in range(3):
if not response.function_calls:
break
# Build tool responses
tool_responses = []
for function_call in response.function_calls:
func_name = function_call.name
func_args = function_call.args
print(f"Agent requested function call: {func_name}({func_args})")
if func_name in TOOLS_MAP:
# Execute the function with arguments
func_to_call = TOOLS_MAP[func_name]
# Convert arguments map into correct keyword arguments
result = func_to_call(**func_args)
else:
result = f"Error: Tool '{func_name}' is not registered."
# Append tool response in correct format
tool_responses.append(
types.Part.from_function_response(
name=func_name,
response={"result": result}
)
)
# Add the model's function calls to content history
contents.append(response.candidates[0].content)
# Add the executed function results to context history
contents.append(types.Content(role="user", parts=tool_responses))
# Send results back to Gemini for final response synthesis or further tool calls
response = client.models.generate_content(
model=CFG.gemini_model,
contents=contents,
config=types.GenerateContentConfig(
system_instruction=system_instruction,
tools=tools_declarations
)
)
return response.text
except Exception as e:
return f"Gemini Agent execution failed: {str(e)}"