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.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- 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()
|