Spaces:
Sleeping
Sleeping
File size: 8,394 Bytes
93e2220 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | """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)}"
|