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)}"