atlas-chat 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Corporate Cars (Fleet) MCP Server
4
+
5
+ Implements a simple RAG-style interface to discover data sources and retrieve
6
+ locations and metadata for corporate cars employees are using.
7
+
8
+ Tools implemented (per RAG_update.md contract):
9
+ - rag_discover_resources(username)
10
+ - rag_get_raw_results(username, query, sources, top_k=8, filters=None, ranking=None)
11
+ - rag_get_synthesized_results(username, query, sources=None, top_k=None, synthesis_params=None, provided_context=None)
12
+
13
+ This is an in-memory example intended for demos and tests.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import datetime as dt
19
+ import time
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, Dict, List, Optional, Tuple
22
+
23
+ from fastmcp import FastMCP
24
+
25
+ # Initialize the MCP server
26
+ mcp = FastMCP("CorporateCars")
27
+
28
+
29
+ # --- In-memory data model ----------------------------------------------------
30
+ @dataclass
31
+ class Car:
32
+ vin: str
33
+ make: str
34
+ model: str
35
+ year: int
36
+ assigned_to: Optional[str] # employee name or None for pool
37
+ department: str # e.g., Sales, Engineering, Exec
38
+ region: str # West, East, Central
39
+ status: str # active, maintenance, offline
40
+ odometer_mi: int
41
+ fuel_pct: int
42
+ last_seen: str # ISO timestamp
43
+ lat: float
44
+ lon: float
45
+ city: str
46
+ tags: List[str] = field(default_factory=list)
47
+
48
+
49
+ # Demo fleet
50
+ NOW = dt.datetime.now(dt.timezone.utc)
51
+
52
+ FLEET: List[Car] = [
53
+ Car(
54
+ vin="1HGBH41JXMN109186",
55
+ make="Toyota",
56
+ model="Camry",
57
+ year=2022,
58
+ assigned_to="Alice Johnson",
59
+ department="Sales",
60
+ region="West",
61
+ status="active",
62
+ odometer_mi=12543,
63
+ fuel_pct=72,
64
+ last_seen=(NOW - dt.timedelta(minutes=14)).isoformat(),
65
+ lat=37.7749,
66
+ lon=-122.4194,
67
+ city="San Francisco, CA",
68
+ tags=["sedan", "hybrid"],
69
+ ),
70
+ Car(
71
+ vin="2FTRX18L1XCA12345",
72
+ make="Ford",
73
+ model="F-150",
74
+ year=2021,
75
+ assigned_to="Bob Smith",
76
+ department="Field Ops",
77
+ region="East",
78
+ status="active",
79
+ odometer_mi=40877,
80
+ fuel_pct=54,
81
+ last_seen=(NOW - dt.timedelta(minutes=3)).isoformat(),
82
+ lat=40.7128,
83
+ lon=-74.0060,
84
+ city="New York, NY",
85
+ tags=["truck", "4x4"],
86
+ ),
87
+ Car(
88
+ vin="3N1AB7AP7GY256789",
89
+ make="Nissan",
90
+ model="Altima",
91
+ year=2019,
92
+ assigned_to=None,
93
+ department="Pool",
94
+ region="Central",
95
+ status="active",
96
+ odometer_mi=58210,
97
+ fuel_pct=33,
98
+ last_seen=(NOW - dt.timedelta(hours=2)).isoformat(),
99
+ lat=41.8781,
100
+ lon=-87.6298,
101
+ city="Chicago, IL",
102
+ tags=["pool", "sedan"],
103
+ ),
104
+ Car(
105
+ vin="WDDGF8AB9EA123456",
106
+ make="Mercedes-Benz",
107
+ model="E350",
108
+ year=2023,
109
+ assigned_to="CEO Vehicle",
110
+ department="Executive",
111
+ region="West",
112
+ status="active",
113
+ odometer_mi=8123,
114
+ fuel_pct=88,
115
+ last_seen=(NOW - dt.timedelta(minutes=25)).isoformat(),
116
+ lat=37.3382,
117
+ lon=-121.8863,
118
+ city="San Jose, CA",
119
+ tags=["executive", "luxury"],
120
+ ),
121
+ ]
122
+
123
+
124
+ # Resources represent logical subsets users can search
125
+ RESOURCES: Dict[str, Dict[str, Any]] = {
126
+ # id -> metadata + predicate
127
+ "west_region": {
128
+ "name": "West Region Fleet",
129
+ "defaultSelected": False,
130
+ "groups": ["users"],
131
+ "predicate": lambda c: c.region == "West",
132
+ },
133
+ "east_region": {
134
+ "name": "East Region Fleet",
135
+ "defaultSelected": False,
136
+ "groups": ["users"],
137
+ "predicate": lambda c: c.region == "East",
138
+ },
139
+ "central_region": {
140
+ "name": "Central Region Fleet",
141
+ "defaultSelected": False,
142
+ "groups": ["users"],
143
+ "predicate": lambda c: c.region == "Central",
144
+ },
145
+ "executive_fleet": {
146
+ "name": "Executive Fleet",
147
+ "defaultSelected": False,
148
+ "groups": ["executive"],
149
+ "predicate": lambda c: "executive" in (c.tags or []),
150
+ },
151
+ "pool_cars": {
152
+ "name": "Pool Cars",
153
+ "defaultSelected": False,
154
+ "groups": ["users"],
155
+ "predicate": lambda c: c.assigned_to is None,
156
+ },
157
+ }
158
+
159
+
160
+ def _start_meta() -> Tuple[float, Dict[str, Any]]:
161
+ return time.perf_counter(), {}
162
+
163
+
164
+ def _done_meta(meta: Dict[str, Any], start: float) -> Dict[str, Any]:
165
+ meta = dict(meta)
166
+ meta["elapsed_ms"] = round((time.perf_counter() - start) * 1000, 3)
167
+ return meta
168
+
169
+
170
+ def _user_default_resource(username: str) -> Optional[str]:
171
+ # Simple demo mapping: choose default resource by known user names
172
+ u = (username or "").strip().lower()
173
+ if not u:
174
+ return None
175
+ if "alice" in u:
176
+ return "west_region"
177
+ if "bob" in u:
178
+ return "east_region"
179
+ return None
180
+
181
+
182
+ def _filter_cars_by_sources(sources: Optional[List[str]]) -> List[Car]:
183
+ if not sources:
184
+ return list(FLEET)
185
+ preds = []
186
+ for sid in sources:
187
+ info = RESOURCES.get(sid)
188
+ if info and callable(info.get("predicate")):
189
+ preds.append(info["predicate"]) # type: ignore[index]
190
+ if not preds:
191
+ return []
192
+ out: List[Car] = []
193
+ for c in FLEET:
194
+ if any(p(c) for p in preds):
195
+ out.append(c)
196
+ return out
197
+
198
+
199
+ def _car_to_hit(c: Car, resource_id: str, score: float, why: str) -> Dict[str, Any]:
200
+ title = f"{c.year} {c.make} {c.model} — {c.city}"
201
+ snippet = (
202
+ f"VIN {c.vin}. Assigned to: {c.assigned_to or 'POOL'}. "
203
+ f"Dept: {c.department}. Region: {c.region}. Status: {c.status}. "
204
+ f"Last seen: {c.last_seen}. {why}"
205
+ )
206
+ return {
207
+ "resourceId": resource_id, # aggregator will qualify with server
208
+ "title": title,
209
+ "snippet": snippet,
210
+ "score": score,
211
+ "location": {
212
+ "lat": c.lat,
213
+ "lon": c.lon,
214
+ "city": c.city,
215
+ },
216
+ "car": {
217
+ "vin": c.vin,
218
+ "make": c.make,
219
+ "model": c.model,
220
+ "year": c.year,
221
+ "assigned_to": c.assigned_to,
222
+ "department": c.department,
223
+ "region": c.region,
224
+ "status": c.status,
225
+ "odometer_mi": c.odometer_mi,
226
+ "fuel_pct": c.fuel_pct,
227
+ "tags": c.tags,
228
+ },
229
+ }
230
+
231
+
232
+ def _search_score(query: str, c: Car) -> Tuple[float, str]:
233
+ """Very simple scoring: count keyword matches across fields."""
234
+ if not query:
235
+ return 0.1, "No query provided"
236
+ q = query.lower()
237
+ score = 0.0
238
+ reasons = []
239
+ fields: List[Tuple[str, str]] = [
240
+ ("employee", (c.assigned_to or "").lower()),
241
+ ("city", c.city.lower()),
242
+ ("make", c.make.lower()),
243
+ ("model", c.model.lower()),
244
+ ("department", c.department.lower()),
245
+ ("region", c.region.lower()),
246
+ ("status", c.status.lower()),
247
+ ("vin", c.vin.lower()),
248
+ ]
249
+ for label, text in fields:
250
+ if text and q in text:
251
+ score += 1.0
252
+ reasons.append(f"match:{label}")
253
+ # Prefer recent last_seen slightly
254
+ try:
255
+ last = dt.datetime.fromisoformat(c.last_seen)
256
+ age_min = max(0.0, (NOW - last).total_seconds() / 60.0)
257
+ score += max(0.0, 0.5 - min(0.5, age_min / 120.0)) # up to +0.5 if < 0 min old
258
+ except Exception:
259
+ # Ignore datetime parsing errors
260
+ pass
261
+ return score, ", ".join(reasons) or "heuristic"
262
+
263
+
264
+ # --- RAG tools ---------------------------------------------------------------
265
+ @mcp.tool
266
+ def rag_discover_resources(username: str) -> Dict[str, Any]:
267
+ """
268
+ Discover available fleet data sources (resources) for this server.
269
+
270
+ Args:
271
+ username: The current user's username (for ACL/defaults purposes)
272
+
273
+ Returns:
274
+ { results: { resources: [ {id, name, authRequired?, defaultSelected?} ] } }
275
+ """
276
+ start, meta = _start_meta()
277
+ try:
278
+ default_sid = _user_default_resource(username)
279
+ resources_ui: List[Dict[str, Any]] = []
280
+ for rid, info in RESOURCES.items():
281
+ resources_ui.append({
282
+ "id": rid,
283
+ "name": info.get("name") or rid,
284
+ # New contract: authRequired is always true, include per-resource groups
285
+ "authRequired": True,
286
+ "groups": list(info.get("groups", [])),
287
+ "defaultSelected": (rid == default_sid) or bool(info.get("defaultSelected", False)),
288
+ })
289
+ return {
290
+ "results": {"resources": resources_ui},
291
+ "meta_data": _done_meta(meta, start),
292
+ }
293
+ except Exception as e:
294
+ return {
295
+ "results": {"resources": [], "error": str(e)},
296
+ "meta_data": _done_meta(meta, start),
297
+ }
298
+
299
+
300
+ def _do_raw_search(
301
+ query: str,
302
+ sources: Optional[List[str]] = None,
303
+ top_k: int = 8,
304
+ filters: Optional[Dict[str, Any]] = None,
305
+ ) -> Dict[str, Any]:
306
+ """Internal function to perform raw search (not decorated as MCP tool)."""
307
+ start, meta = _start_meta()
308
+ filters = filters or {}
309
+ try:
310
+ cars = _filter_cars_by_sources(sources)
311
+
312
+ # Apply simple filters
313
+ dept = (filters.get("department") or "").lower() if isinstance(filters, dict) else ""
314
+ status = (filters.get("status") or "").lower() if isinstance(filters, dict) else ""
315
+ if dept:
316
+ cars = [c for c in cars if c.department.lower() == dept]
317
+ if status:
318
+ cars = [c for c in cars if c.status.lower() == status]
319
+
320
+ # Score and assemble hits
321
+ scored: List[Tuple[float, Dict[str, Any]]] = []
322
+ for c in cars:
323
+ s, why = _search_score(query or "", c)
324
+ if s <= 0:
325
+ continue
326
+ # Choose the first matching source id for this car for provenance
327
+ rid = None
328
+ for sid, info in RESOURCES.items():
329
+ pred = info.get("predicate")
330
+ if callable(pred) and pred(c):
331
+ rid = sid
332
+ break
333
+ rid = rid or (sources[0] if sources else "fleet")
334
+ hit = _car_to_hit(c, rid, score=float(s), why=why)
335
+ scored.append((float(s), hit))
336
+
337
+ scored.sort(key=lambda t: t[0], reverse=True)
338
+ hits = [h for _, h in scored[: (top_k or len(scored))]]
339
+
340
+ return {
341
+ "results": {
342
+ "hits": hits,
343
+ "stats": {"total": len(scored), "returned": len(hits)},
344
+ },
345
+ "meta_data": _done_meta(meta, start),
346
+ }
347
+ except Exception as e:
348
+ return {
349
+ "results": {"hits": [], "error": str(e)},
350
+ "meta_data": _done_meta(meta, start),
351
+ }
352
+
353
+
354
+ @mcp.tool
355
+ def rag_get_raw_results(
356
+ username: str,
357
+ query: str,
358
+ sources: Optional[List[str]] = None,
359
+ top_k: int = 8,
360
+ filters: Optional[Dict[str, Any]] = None,
361
+ ranking: Optional[Dict[str, Any]] = None,
362
+ ) -> Dict[str, Any]:
363
+ """
364
+ Search fleet data and return raw hits with location and car metadata.
365
+
366
+ Args mirror the expected contract from the aggregator. "sources" are the
367
+ resource IDs from rag_discover_resources.
368
+ """
369
+ # Delegate to internal function (ranking is ignored in this simple impl)
370
+ return _do_raw_search(query, sources, top_k, filters)
371
+
372
+
373
+ @mcp.tool
374
+ def rag_get_synthesized_results(
375
+ username: str,
376
+ query: str,
377
+ sources: Optional[List[str]] = None,
378
+ top_k: Optional[int] = None,
379
+ synthesis_params: Optional[Dict[str, Any]] = None,
380
+ provided_context: Optional[Dict[str, Any]] = None,
381
+ ) -> Dict[str, Any]:
382
+ """
383
+ Return a simple synthesized answer about car locations.
384
+ """
385
+ start, meta = _start_meta()
386
+ try:
387
+ # Reuse raw results to build a succinct answer (use internal function, not the decorated tool)
388
+ raw = _do_raw_search(
389
+ query=query,
390
+ sources=sources,
391
+ top_k=(top_k or 5),
392
+ filters=(synthesis_params or {}).get("filters") if synthesis_params else None,
393
+ )
394
+ hits = ((raw.get("results") or {}).get("hits") or [])
395
+ if not hits:
396
+ return {
397
+ "results": {"answer": "No matching vehicles found.", "citations": []},
398
+ "meta_data": _done_meta(meta, start),
399
+ }
400
+
401
+ # Build a sentence per hit
402
+ lines: List[str] = []
403
+ cits: List[Dict[str, Any]] = []
404
+ for h in hits:
405
+ car = (h.get("car") or {})
406
+ loc = (h.get("location") or {})
407
+ who = car.get("assigned_to") or "POOL"
408
+ city = loc.get("city") or "(unknown)"
409
+ vin = car.get("vin")
410
+ line = f"{who}'s {car.get('year')} {car.get('make')} {car.get('model')} is in {city}."
411
+ lines.append(line)
412
+ cits.append({
413
+ "resourceId": h.get("resourceId"),
414
+ "snippet": h.get("snippet"),
415
+ "car": car,
416
+ "location": loc,
417
+ "vin": vin,
418
+ })
419
+
420
+ answer = "\n".join(lines[: (top_k or 3)])
421
+ return {
422
+ "results": {
423
+ "answer": answer,
424
+ "citations": cits,
425
+ "limits": {"truncated": len(lines) > (top_k or 3)},
426
+ },
427
+ "meta_data": _done_meta(meta, start),
428
+ }
429
+ except Exception as e:
430
+ return {
431
+ "results": {"answer": "", "error": str(e)},
432
+ "meta_data": _done_meta(meta, start),
433
+ }
434
+
435
+
436
+ if __name__ == "__main__":
437
+ mcp.run()