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,24 @@
|
|
|
1
|
+
"""RAG module for the chat backend.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- RAG query processing and context retrieval
|
|
5
|
+
- Document metadata and search capabilities
|
|
6
|
+
- Integration with ATLAS RAG API
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .atlas_rag_client import AtlasRAGClient, create_atlas_rag_client_from_config
|
|
10
|
+
from .client import DataSource, DocumentMetadata, RAGClient, RAGMetadata, RAGResponse
|
|
11
|
+
|
|
12
|
+
# Create default instance
|
|
13
|
+
rag_client = RAGClient()
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"RAGClient",
|
|
17
|
+
"AtlasRAGClient",
|
|
18
|
+
"create_atlas_rag_client_from_config",
|
|
19
|
+
"DataSource",
|
|
20
|
+
"DocumentMetadata",
|
|
21
|
+
"RAGMetadata",
|
|
22
|
+
"RAGResponse",
|
|
23
|
+
"rag_client",
|
|
24
|
+
]
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""ATLAS RAG Client for integrating with the ATLAS RAG API.
|
|
2
|
+
|
|
3
|
+
This client implements the same interface as RAGClient but translates
|
|
4
|
+
requests to the ATLAS RAG API format.
|
|
5
|
+
|
|
6
|
+
ATLAS RAG API:
|
|
7
|
+
- Discovery: GET /discover/datasources?as_user={user}
|
|
8
|
+
- Query: POST /rag/completions?as_user={user}
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from fastapi import HTTPException
|
|
16
|
+
|
|
17
|
+
from atlas.modules.rag.client import DataSource, DocumentMetadata, RAGMetadata, RAGResponse
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AtlasRAGClient:
|
|
23
|
+
"""Client for communicating with external ATLAS RAG API.
|
|
24
|
+
|
|
25
|
+
Implements the same interface as RAGClient for seamless substitution.
|
|
26
|
+
Uses Bearer token authentication with user impersonation via as_user param.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
base_url: str,
|
|
32
|
+
bearer_token: Optional[str] = None,
|
|
33
|
+
default_model: str = "openai/gpt-oss-120b",
|
|
34
|
+
top_k: int = 4,
|
|
35
|
+
timeout: float = 60.0,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize the external RAG client.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
base_url: Base URL for the external RAG API.
|
|
41
|
+
bearer_token: Bearer token for API authentication.
|
|
42
|
+
default_model: Default model to use for RAG queries.
|
|
43
|
+
top_k: Default number of documents to retrieve.
|
|
44
|
+
timeout: Request timeout in seconds.
|
|
45
|
+
"""
|
|
46
|
+
self.base_url = base_url.rstrip("/")
|
|
47
|
+
self.bearer_token = bearer_token
|
|
48
|
+
self.default_model = default_model
|
|
49
|
+
self.top_k = top_k
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
|
|
52
|
+
logger.info(
|
|
53
|
+
"AtlasRAGClient initialized: url=%s, model=%s, top_k=%d",
|
|
54
|
+
self.base_url,
|
|
55
|
+
self.default_model,
|
|
56
|
+
self.top_k,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
60
|
+
"""Build HTTP headers for API requests."""
|
|
61
|
+
headers = {"Content-Type": "application/json"}
|
|
62
|
+
if self.bearer_token:
|
|
63
|
+
headers["Authorization"] = f"Bearer {self.bearer_token}"
|
|
64
|
+
return headers
|
|
65
|
+
|
|
66
|
+
async def discover_data_sources(self, user_name: str) -> List[DataSource]:
|
|
67
|
+
"""Discover data sources accessible by a user.
|
|
68
|
+
|
|
69
|
+
Calls GET /discover/datasources?as_user={user_name}
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
user_name: The username to discover data sources for.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of DataSource objects the user can access.
|
|
76
|
+
"""
|
|
77
|
+
logger.info("Discovering data sources for user: %s", user_name)
|
|
78
|
+
|
|
79
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
80
|
+
try:
|
|
81
|
+
response = await client.get(
|
|
82
|
+
f"{self.base_url}/discover/datasources",
|
|
83
|
+
headers=self._get_headers(),
|
|
84
|
+
params={"as_user": user_name},
|
|
85
|
+
)
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
data = response.json()
|
|
88
|
+
|
|
89
|
+
# Response format: {user_name: str, accessible_data_sources: [{name, compliance_level}]}
|
|
90
|
+
accessible_sources = data.get("accessible_data_sources", [])
|
|
91
|
+
data_sources = [DataSource(**src) for src in accessible_sources]
|
|
92
|
+
|
|
93
|
+
logger.info(
|
|
94
|
+
"Discovered %d data sources for user %s",
|
|
95
|
+
len(data_sources),
|
|
96
|
+
user_name,
|
|
97
|
+
)
|
|
98
|
+
return data_sources
|
|
99
|
+
|
|
100
|
+
except httpx.HTTPStatusError as exc:
|
|
101
|
+
logger.error(
|
|
102
|
+
"HTTP error discovering data sources for %s: %s (status %d)",
|
|
103
|
+
user_name,
|
|
104
|
+
exc.response.text,
|
|
105
|
+
exc.response.status_code,
|
|
106
|
+
)
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
except httpx.RequestError as exc:
|
|
110
|
+
logger.error(
|
|
111
|
+
"Request error discovering data sources for %s: %s",
|
|
112
|
+
user_name,
|
|
113
|
+
str(exc),
|
|
114
|
+
)
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
logger.error(
|
|
119
|
+
"Unexpected error discovering data sources for %s: %s",
|
|
120
|
+
user_name,
|
|
121
|
+
str(exc),
|
|
122
|
+
exc_info=True,
|
|
123
|
+
)
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
async def query_rag(
|
|
127
|
+
self, user_name: str, data_source: str, messages: List[Dict]
|
|
128
|
+
) -> RAGResponse:
|
|
129
|
+
"""Query RAG endpoint for a response with metadata.
|
|
130
|
+
|
|
131
|
+
Calls POST /rag/completions?as_user={user_name}
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
user_name: The username making the query.
|
|
135
|
+
data_source: The data source (corpus) to query.
|
|
136
|
+
messages: List of message dictionaries with role and content.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
RAGResponse containing content and optional metadata.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
HTTPException: On API errors (403, 404, 500).
|
|
143
|
+
"""
|
|
144
|
+
logger.info(
|
|
145
|
+
"[HTTP-RAG] query_rag called: user=%s, data_source=%s, message_count=%d",
|
|
146
|
+
user_name,
|
|
147
|
+
data_source,
|
|
148
|
+
len(messages),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Extract user query for logging
|
|
152
|
+
user_query = ""
|
|
153
|
+
for msg in reversed(messages):
|
|
154
|
+
if msg.get("role") == "user":
|
|
155
|
+
user_query = msg.get("content", "")[:100]
|
|
156
|
+
break
|
|
157
|
+
logger.debug(
|
|
158
|
+
"[HTTP-RAG] Query preview: %s...",
|
|
159
|
+
user_query,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Build request payload matching RagRequest format
|
|
163
|
+
payload = {
|
|
164
|
+
"messages": messages,
|
|
165
|
+
"stream": False,
|
|
166
|
+
"model": self.default_model,
|
|
167
|
+
"top_k": self.top_k,
|
|
168
|
+
"corpora": [data_source] if data_source else None,
|
|
169
|
+
"threshold": None,
|
|
170
|
+
"expanded_window": [0, 0],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
logger.debug(
|
|
174
|
+
"[HTTP-RAG] Request payload: model=%s, top_k=%d, corpora=%s",
|
|
175
|
+
payload["model"],
|
|
176
|
+
payload["top_k"],
|
|
177
|
+
payload["corpora"],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
181
|
+
try:
|
|
182
|
+
response = await client.post(
|
|
183
|
+
f"{self.base_url}/rag/completions",
|
|
184
|
+
headers=self._get_headers(),
|
|
185
|
+
params={"as_user": user_name},
|
|
186
|
+
json=payload,
|
|
187
|
+
)
|
|
188
|
+
response.raise_for_status()
|
|
189
|
+
data = response.json()
|
|
190
|
+
|
|
191
|
+
logger.debug(
|
|
192
|
+
"[HTTP-RAG] Response received: status=%d, keys=%s",
|
|
193
|
+
response.status_code,
|
|
194
|
+
list(data.keys()),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Check if this is a chat completion (already LLM-interpreted)
|
|
198
|
+
is_completion = data.get("object") == "chat.completion"
|
|
199
|
+
|
|
200
|
+
# Extract content from OpenAI ChatCompletion format
|
|
201
|
+
content = "No response from RAG system."
|
|
202
|
+
if "choices" in data and len(data["choices"]) > 0:
|
|
203
|
+
choice = data["choices"][0]
|
|
204
|
+
if "message" in choice and "content" in choice["message"]:
|
|
205
|
+
content = choice["message"]["content"]
|
|
206
|
+
|
|
207
|
+
logger.debug(
|
|
208
|
+
"[HTTP-RAG] Extracted content: length=%d, is_completion=%s, preview=%s...",
|
|
209
|
+
len(content),
|
|
210
|
+
is_completion,
|
|
211
|
+
content[:300] if content else "(empty)",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Map rag_metadata to RAGMetadata
|
|
215
|
+
metadata = self._parse_rag_metadata(data, data_source)
|
|
216
|
+
|
|
217
|
+
logger.info(
|
|
218
|
+
"[HTTP-RAG] query_rag complete: user=%s, source=%s, content_length=%d, has_metadata=%s, is_completion=%s",
|
|
219
|
+
user_name,
|
|
220
|
+
data_source,
|
|
221
|
+
len(content),
|
|
222
|
+
metadata is not None,
|
|
223
|
+
is_completion,
|
|
224
|
+
)
|
|
225
|
+
return RAGResponse(content=content, metadata=metadata, is_completion=is_completion)
|
|
226
|
+
|
|
227
|
+
except httpx.HTTPStatusError as exc:
|
|
228
|
+
status_code = exc.response.status_code
|
|
229
|
+
logger.error(
|
|
230
|
+
"HTTP error querying RAG for %s: %s (status %d)",
|
|
231
|
+
user_name,
|
|
232
|
+
exc.response.text,
|
|
233
|
+
status_code,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if status_code == 403:
|
|
237
|
+
raise HTTPException(
|
|
238
|
+
status_code=403, detail="Access denied to data source"
|
|
239
|
+
)
|
|
240
|
+
elif status_code == 404:
|
|
241
|
+
raise HTTPException(
|
|
242
|
+
status_code=404, detail="Data source not found"
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
raise HTTPException(
|
|
246
|
+
status_code=500, detail="RAG service error"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
except httpx.RequestError as exc:
|
|
250
|
+
logger.error(
|
|
251
|
+
"Request error querying RAG for %s: %s",
|
|
252
|
+
user_name,
|
|
253
|
+
str(exc),
|
|
254
|
+
)
|
|
255
|
+
raise HTTPException(
|
|
256
|
+
status_code=500, detail="Failed to connect to RAG service"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
except HTTPException:
|
|
260
|
+
# Re-raise HTTPExceptions
|
|
261
|
+
raise
|
|
262
|
+
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
logger.error(
|
|
265
|
+
"Unexpected error querying RAG for %s: %s",
|
|
266
|
+
user_name,
|
|
267
|
+
str(exc),
|
|
268
|
+
exc_info=True,
|
|
269
|
+
)
|
|
270
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
271
|
+
|
|
272
|
+
def _parse_rag_metadata(
|
|
273
|
+
self, data: Dict, data_source: str
|
|
274
|
+
) -> Optional[RAGMetadata]:
|
|
275
|
+
"""Parse rag_metadata from API response into RAGMetadata model.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
data: The full API response dictionary.
|
|
279
|
+
data_source: The data source used in the query.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
RAGMetadata if present in response, None otherwise.
|
|
283
|
+
"""
|
|
284
|
+
if "rag_metadata" not in data or not data["rag_metadata"]:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
rm = data["rag_metadata"]
|
|
289
|
+
|
|
290
|
+
# Map documents_found to DocumentMetadata list
|
|
291
|
+
documents_found = []
|
|
292
|
+
for doc in rm.get("documents_found", []):
|
|
293
|
+
doc_metadata = DocumentMetadata(
|
|
294
|
+
source=doc.get("corpus_id", ""),
|
|
295
|
+
content_type=doc.get("content_type", "atlas-search"),
|
|
296
|
+
confidence_score=doc.get("confidence_score", 0.0),
|
|
297
|
+
chunk_id=str(doc.get("id")) if doc.get("id") else None,
|
|
298
|
+
last_modified=doc.get("last_modified"),
|
|
299
|
+
)
|
|
300
|
+
documents_found.append(doc_metadata)
|
|
301
|
+
|
|
302
|
+
# Determine data source name from response or fallback
|
|
303
|
+
data_sources_list = rm.get("data_sources", [])
|
|
304
|
+
data_source_name = (
|
|
305
|
+
data_sources_list[0] if data_sources_list else data_source
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return RAGMetadata(
|
|
309
|
+
query_processing_time_ms=rm.get("query_processing_time_ms", 0),
|
|
310
|
+
total_documents_searched=len(documents_found),
|
|
311
|
+
documents_found=documents_found,
|
|
312
|
+
data_source_name=data_source_name,
|
|
313
|
+
retrieval_method=rm.get("retrieval_method", "similarity"),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.warning("Failed to parse RAG metadata: %s", str(e))
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def create_atlas_rag_client_from_config(config_manager) -> AtlasRAGClient:
|
|
322
|
+
"""Factory function to create AtlasRAGClient from ConfigManager.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
config_manager: ConfigManager instance with app_settings.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Configured AtlasRAGClient instance.
|
|
329
|
+
"""
|
|
330
|
+
settings = config_manager.app_settings
|
|
331
|
+
return AtlasRAGClient(
|
|
332
|
+
base_url=settings.external_rag_url,
|
|
333
|
+
bearer_token=settings.external_rag_bearer_token,
|
|
334
|
+
default_model=settings.external_rag_default_model,
|
|
335
|
+
top_k=settings.external_rag_top_k,
|
|
336
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""RAG Client for integrating with RAG mock endpoint."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import HTTPException
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from atlas.core.http_client import create_rag_client
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataSource(BaseModel):
|
|
13
|
+
"""Represents a RAG data source with compliance information."""
|
|
14
|
+
name: str
|
|
15
|
+
compliance_level: str
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DocumentMetadata(BaseModel):
|
|
21
|
+
"""Metadata about a source document."""
|
|
22
|
+
source: str
|
|
23
|
+
content_type: str
|
|
24
|
+
confidence_score: float
|
|
25
|
+
chunk_id: Optional[str] = None
|
|
26
|
+
last_modified: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RAGMetadata(BaseModel):
|
|
30
|
+
"""Metadata about RAG query processing."""
|
|
31
|
+
query_processing_time_ms: int
|
|
32
|
+
total_documents_searched: int
|
|
33
|
+
documents_found: List[DocumentMetadata]
|
|
34
|
+
data_source_name: str
|
|
35
|
+
retrieval_method: str
|
|
36
|
+
query_embedding_time_ms: Optional[int] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RAGResponse(BaseModel):
|
|
40
|
+
"""Combined response from RAG system including content and metadata."""
|
|
41
|
+
content: str
|
|
42
|
+
metadata: Optional[RAGMetadata] = None
|
|
43
|
+
is_completion: bool = False # True if content is already LLM-interpreted (from /rag/completions)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RAGClient:
|
|
47
|
+
"""Legacy RAG client for the old rag-mock service.
|
|
48
|
+
|
|
49
|
+
Note: This client is deprecated. Use UnifiedRAGService for RAG operations,
|
|
50
|
+
which handles all RAG sources configured in rag-sources.json.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, base_url: str = "http://localhost:8001", timeout: float = 30.0):
|
|
54
|
+
"""Initialize the legacy RAG client.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
base_url: Base URL for the RAG mock service.
|
|
58
|
+
timeout: Request timeout in seconds.
|
|
59
|
+
"""
|
|
60
|
+
self.base_url = base_url
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
self.test_client = None
|
|
63
|
+
self.http_client = create_rag_client(self.base_url, self.timeout)
|
|
64
|
+
logger.warning(
|
|
65
|
+
"RAGClient is deprecated. Use UnifiedRAGService for RAG operations. "
|
|
66
|
+
"Configure RAG sources in rag-sources.json."
|
|
67
|
+
)
|
|
68
|
+
logger.info("RAGClient initialized with URL: %s", self.base_url)
|
|
69
|
+
|
|
70
|
+
async def discover_data_sources(self, user_name: str) -> List[DataSource]:
|
|
71
|
+
"""Discover data sources accessible by a user.
|
|
72
|
+
|
|
73
|
+
Note: This method is deprecated. Use UnifiedRAGService.discover_data_sources() instead.
|
|
74
|
+
"""
|
|
75
|
+
logger.info("discover_data_sources: user=%s (deprecated RAGClient)", user_name)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
data = await self.http_client.get(f"/v1/discover/datasources/{user_name}")
|
|
79
|
+
accessible_sources_data = data.get("accessible_data_sources", [])
|
|
80
|
+
except HTTPException as exc:
|
|
81
|
+
logger.warning("HTTP error discovering data sources for %s: %s", user_name, exc.detail)
|
|
82
|
+
return []
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
logger.error("Unexpected error while discovering data sources for %s: %s", user_name, exc, exc_info=True)
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
return [DataSource(**source_data) for source_data in accessible_sources_data]
|
|
88
|
+
|
|
89
|
+
async def query_rag(self, user_name: str, data_source: str, messages: List[Dict]) -> RAGResponse:
|
|
90
|
+
"""Query RAG endpoint for a response with metadata.
|
|
91
|
+
|
|
92
|
+
Note: This method is deprecated. Use UnifiedRAGService.query_rag() instead.
|
|
93
|
+
"""
|
|
94
|
+
payload = {
|
|
95
|
+
"messages": messages,
|
|
96
|
+
"user_name": user_name,
|
|
97
|
+
"data_source": data_source,
|
|
98
|
+
"model": "gpt-4-rag-mock",
|
|
99
|
+
"stream": False
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.info("query_rag: user=%s, source=%s (deprecated RAGClient)", user_name, data_source)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
data = await self.http_client.post("/v1/chat/completions", json_data=payload)
|
|
106
|
+
|
|
107
|
+
# Extract the assistant message from the response
|
|
108
|
+
content = "No response from RAG system."
|
|
109
|
+
if "choices" in data and len(data["choices"]) > 0:
|
|
110
|
+
choice = data["choices"][0]
|
|
111
|
+
if "message" in choice and "content" in choice["message"]:
|
|
112
|
+
content = choice["message"]["content"]
|
|
113
|
+
|
|
114
|
+
# Extract metadata if present
|
|
115
|
+
metadata = None
|
|
116
|
+
if "rag_metadata" in data and data["rag_metadata"]:
|
|
117
|
+
try:
|
|
118
|
+
metadata = RAGMetadata(**data["rag_metadata"])
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.warning(f"Failed to parse RAG metadata: {e}")
|
|
121
|
+
|
|
122
|
+
return RAGResponse(content=content, metadata=metadata)
|
|
123
|
+
|
|
124
|
+
except HTTPException:
|
|
125
|
+
# Re-raise HTTPExceptions from the unified client (they already have proper error handling)
|
|
126
|
+
raise
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
logger.error("Unexpected error while querying RAG for %s: %s", user_name, exc, exc_info=True)
|
|
129
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|