lightspeed-stack 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.
- app/__init__.py +1 -0
- app/endpoints/.ruff_cache/.gitignore +2 -0
- app/endpoints/.ruff_cache/0.9.1/5703048272820174433 +0 -0
- app/endpoints/.ruff_cache/0.9.1/9961612457335986079 +0 -0
- app/endpoints/.ruff_cache/CACHEDIR.TAG +1 -0
- app/endpoints/__init__.py +1 -0
- app/endpoints/config.py +64 -0
- app/endpoints/feedback.py +129 -0
- app/endpoints/health.py +111 -0
- app/endpoints/info.py +26 -0
- app/endpoints/models.py +79 -0
- app/endpoints/query.py +360 -0
- app/endpoints/root.py +777 -0
- app/endpoints/streaming_query.py +321 -0
- app/main.py +38 -0
- app/routers.py +30 -0
- auth/__init__.py +38 -0
- auth/interface.py +13 -0
- auth/k8s.py +270 -0
- auth/noop.py +42 -0
- auth/noop_with_token.py +46 -0
- auth/utils.py +26 -0
- lightspeed_stack-0.1.0.dist-info/METADATA +443 -0
- lightspeed_stack-0.1.0.dist-info/RECORD +43 -0
- lightspeed_stack-0.1.0.dist-info/WHEEL +4 -0
- lightspeed_stack-0.1.0.dist-info/entry_points.txt +4 -0
- lightspeed_stack-0.1.0.dist-info/licenses/LICENSE +201 -0
- models/__init__.py +1 -0
- models/config.py +161 -0
- models/requests.py +208 -0
- models/responses.py +244 -0
- runners/__init__.py +1 -0
- runners/uvicorn.py +31 -0
- utils/.ruff_cache/.gitignore +2 -0
- utils/.ruff_cache/0.9.1/18446581155718949728 +0 -0
- utils/.ruff_cache/0.9.1/4991844299736624256 +0 -0
- utils/.ruff_cache/CACHEDIR.TAG +1 -0
- utils/__init__.py +1 -0
- utils/checks.py +27 -0
- utils/common.py +111 -0
- utils/endpoints.py +34 -0
- utils/mcp_headers.py +48 -0
- utils/suid.py +28 -0
models/responses.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Models for service responses."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelsResponse(BaseModel):
|
|
9
|
+
"""Model representing a response to models request."""
|
|
10
|
+
|
|
11
|
+
models: list[dict[str, Any]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# TODO(lucasagomes): a lot of fields to add to QueryResponse. For now
|
|
15
|
+
# we are keeping it simple. The missing fields are:
|
|
16
|
+
# - referenced_documents: The optional URLs and titles for the documents used
|
|
17
|
+
# to generate the response.
|
|
18
|
+
# - truncated: Set to True if conversation history was truncated to be within context window.
|
|
19
|
+
# - input_tokens: Number of tokens sent to LLM
|
|
20
|
+
# - output_tokens: Number of tokens received from LLM
|
|
21
|
+
# - available_quotas: Quota available as measured by all configured quota limiters
|
|
22
|
+
# - tool_calls: List of tool requests.
|
|
23
|
+
# - tool_results: List of tool results.
|
|
24
|
+
# See LLMResponse in ols-service for more details.
|
|
25
|
+
class QueryResponse(BaseModel):
|
|
26
|
+
"""Model representing LLM response to a query.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
conversation_id: The optional conversation ID (UUID).
|
|
30
|
+
response: The response.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
conversation_id: Optional[str] = None
|
|
34
|
+
response: str
|
|
35
|
+
|
|
36
|
+
# provides examples for /docs endpoint
|
|
37
|
+
model_config = {
|
|
38
|
+
"json_schema_extra": {
|
|
39
|
+
"examples": [
|
|
40
|
+
{
|
|
41
|
+
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
|
|
42
|
+
"response": "Operator Lifecycle Manager (OLM) helps users install...",
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InfoResponse(BaseModel):
|
|
50
|
+
"""Model representing a response to a info request.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
name: Service name.
|
|
54
|
+
version: Service version.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
```python
|
|
58
|
+
info_response = InfoResponse(
|
|
59
|
+
name="Lightspeed Stack",
|
|
60
|
+
version="1.0.0",
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
version: str
|
|
67
|
+
|
|
68
|
+
# provides examples for /docs endpoint
|
|
69
|
+
model_config = {
|
|
70
|
+
"json_schema_extra": {
|
|
71
|
+
"examples": [
|
|
72
|
+
{
|
|
73
|
+
"name": "Lightspeed Stack",
|
|
74
|
+
"version": "1.0.0",
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ProviderHealthStatus(BaseModel):
|
|
82
|
+
"""Model representing the health status of a provider.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
provider_id: The ID of the provider.
|
|
86
|
+
status: The health status ('ok', 'unhealthy', 'not_implemented').
|
|
87
|
+
message: Optional message about the health status.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
provider_id: str
|
|
91
|
+
status: str
|
|
92
|
+
message: Optional[str] = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ReadinessResponse(BaseModel):
|
|
96
|
+
"""Model representing response to a readiness request.
|
|
97
|
+
|
|
98
|
+
Attributes:
|
|
99
|
+
ready: If service is ready.
|
|
100
|
+
reason: The reason for the readiness.
|
|
101
|
+
providers: List of unhealthy providers in case of readiness failure.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
```python
|
|
105
|
+
readiness_response = ReadinessResponse(
|
|
106
|
+
ready=False,
|
|
107
|
+
reason="Service is not ready",
|
|
108
|
+
providers=[
|
|
109
|
+
ProviderHealthStatus(
|
|
110
|
+
provider_id="ollama",
|
|
111
|
+
status="Error",
|
|
112
|
+
message="Server is unavailable"
|
|
113
|
+
)
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
ready: bool
|
|
120
|
+
reason: str
|
|
121
|
+
providers: list[ProviderHealthStatus]
|
|
122
|
+
|
|
123
|
+
# provides examples for /docs endpoint
|
|
124
|
+
model_config = {
|
|
125
|
+
"json_schema_extra": {
|
|
126
|
+
"examples": [
|
|
127
|
+
{
|
|
128
|
+
"ready": True,
|
|
129
|
+
"reason": "Service is ready",
|
|
130
|
+
"providers": [],
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class LivenessResponse(BaseModel):
|
|
138
|
+
"""Model representing a response to a liveness request.
|
|
139
|
+
|
|
140
|
+
Attributes:
|
|
141
|
+
alive: If app is alive.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
```python
|
|
145
|
+
liveness_response = LivenessResponse(alive=True)
|
|
146
|
+
```
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
alive: bool
|
|
150
|
+
|
|
151
|
+
# provides examples for /docs endpoint
|
|
152
|
+
model_config = {
|
|
153
|
+
"json_schema_extra": {
|
|
154
|
+
"examples": [
|
|
155
|
+
{
|
|
156
|
+
"alive": True,
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class NotAvailableResponse(BaseModel):
|
|
164
|
+
"""Model representing error response for readiness endpoint."""
|
|
165
|
+
|
|
166
|
+
detail: dict[str, str]
|
|
167
|
+
|
|
168
|
+
# provides examples for /docs endpoint
|
|
169
|
+
model_config = {
|
|
170
|
+
"json_schema_extra": {
|
|
171
|
+
"examples": [
|
|
172
|
+
{
|
|
173
|
+
"detail": {
|
|
174
|
+
"response": "Service is not ready",
|
|
175
|
+
"cause": "Index is not ready",
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"detail": {
|
|
180
|
+
"response": "Service is not ready",
|
|
181
|
+
"cause": "LLM is not ready",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class FeedbackResponse(BaseModel):
|
|
190
|
+
"""Model representing a response to a feedback request.
|
|
191
|
+
|
|
192
|
+
Attributes:
|
|
193
|
+
response: The response of the feedback request.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
```python
|
|
197
|
+
feedback_response = FeedbackResponse(response="feedback received")
|
|
198
|
+
```
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
response: str
|
|
202
|
+
|
|
203
|
+
# provides examples for /docs endpoint
|
|
204
|
+
model_config = {
|
|
205
|
+
"json_schema_extra": {
|
|
206
|
+
"examples": [
|
|
207
|
+
{
|
|
208
|
+
"response": "feedback received",
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class StatusResponse(BaseModel):
|
|
216
|
+
"""Model representing a response to a status request.
|
|
217
|
+
|
|
218
|
+
Attributes:
|
|
219
|
+
functionality: The functionality of the service.
|
|
220
|
+
status: The status of the service.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
```python
|
|
224
|
+
status_response = StatusResponse(
|
|
225
|
+
functionality="feedback",
|
|
226
|
+
status={"enabled": True},
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
functionality: str
|
|
232
|
+
status: dict
|
|
233
|
+
|
|
234
|
+
# provides examples for /docs endpoint
|
|
235
|
+
model_config = {
|
|
236
|
+
"json_schema_extra": {
|
|
237
|
+
"examples": [
|
|
238
|
+
{
|
|
239
|
+
"functionality": "feedback",
|
|
240
|
+
"status": {"enabled": True},
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
}
|
runners/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runners."""
|
runners/uvicorn.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Uvicorn runner."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import uvicorn
|
|
6
|
+
|
|
7
|
+
from models.config import ServiceConfiguration
|
|
8
|
+
|
|
9
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def start_uvicorn(configuration: ServiceConfiguration) -> None:
|
|
13
|
+
"""Start Uvicorn-based REST API service."""
|
|
14
|
+
logger.info("Starting Uvicorn")
|
|
15
|
+
|
|
16
|
+
log_level = logging.INFO
|
|
17
|
+
|
|
18
|
+
# please note:
|
|
19
|
+
# TLS fields can be None, which means we will pass those values as None to uvicorn.run
|
|
20
|
+
uvicorn.run(
|
|
21
|
+
"app.main:app",
|
|
22
|
+
host=configuration.host,
|
|
23
|
+
port=configuration.port,
|
|
24
|
+
workers=configuration.workers,
|
|
25
|
+
log_level=log_level,
|
|
26
|
+
ssl_keyfile=configuration.tls_config.tls_key_path,
|
|
27
|
+
ssl_certfile=configuration.tls_config.tls_certificate_path,
|
|
28
|
+
ssl_keyfile_password=str(configuration.tls_config.tls_key_password or ""),
|
|
29
|
+
use_colors=True,
|
|
30
|
+
access_log=True,
|
|
31
|
+
)
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Signature: 8a477f597d28d172789f06886806bc55
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utils."""
|
utils/checks.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Checks that are performed to configuration options."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import FilePath
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidConfigurationError(Exception):
|
|
10
|
+
"""Lightspeed configuration is invalid."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_attribute_from_file(data: dict, file_name_key: str) -> Optional[str]:
|
|
14
|
+
"""Retrieve value of an attribute from a file."""
|
|
15
|
+
file_path = data.get(file_name_key)
|
|
16
|
+
if file_path is not None:
|
|
17
|
+
with open(file_path, encoding="utf-8") as f:
|
|
18
|
+
return f.read().rstrip()
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def file_check(path: FilePath, desc: str) -> None:
|
|
23
|
+
"""Check that path is a readable regular file."""
|
|
24
|
+
if not os.path.isfile(path):
|
|
25
|
+
raise InvalidConfigurationError(f"{desc} '{path}' is not a file")
|
|
26
|
+
if not os.access(path, os.R_OK):
|
|
27
|
+
raise InvalidConfigurationError(f"{desc} '{path}' is not readable")
|
utils/common.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Common utilities for the project."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, List, cast
|
|
4
|
+
from logging import Logger
|
|
5
|
+
|
|
6
|
+
from llama_stack_client import LlamaStackClient
|
|
7
|
+
|
|
8
|
+
from llama_stack.distribution.library_client import (
|
|
9
|
+
LlamaStackAsLibraryClient,
|
|
10
|
+
AsyncLlamaStackAsLibraryClient,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from client import get_llama_stack_client
|
|
14
|
+
from models.config import Configuration, ModelContextProtocolServer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# TODO(lucasagomes): implement this function to retrieve user ID from auth
|
|
18
|
+
def retrieve_user_id(auth: Any) -> str: # pylint: disable=unused-argument
|
|
19
|
+
"""Retrieve the user ID from the authentication handler.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
auth: The Authentication handler (FastAPI Depends) that will
|
|
23
|
+
handle authentication Logic.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
str: The user ID.
|
|
27
|
+
"""
|
|
28
|
+
return "user_id_placeholder"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def register_mcp_servers_async(
|
|
32
|
+
logger: Logger, configuration: Configuration
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Register Model Context Protocol (MCP) servers with the LlamaStack client (async)."""
|
|
35
|
+
# Skip MCP registration if no MCP servers are configured
|
|
36
|
+
if not configuration.mcp_servers:
|
|
37
|
+
logger.debug("No MCP servers configured, skipping registration")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if configuration.llama_stack.use_as_library_client:
|
|
41
|
+
# Library client - use async interface
|
|
42
|
+
# config.py validation ensures library_client_config_path is not None
|
|
43
|
+
# when use_as_library_client is True
|
|
44
|
+
config_path = cast(str, configuration.llama_stack.library_client_config_path)
|
|
45
|
+
client = LlamaStackAsLibraryClient(config_path)
|
|
46
|
+
await client.async_client.initialize()
|
|
47
|
+
|
|
48
|
+
await _register_mcp_toolgroups_async(
|
|
49
|
+
client.async_client, configuration.mcp_servers, logger
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
# Service client - use sync interface
|
|
53
|
+
client = get_llama_stack_client(configuration.llama_stack)
|
|
54
|
+
|
|
55
|
+
_register_mcp_toolgroups_sync(client, configuration.mcp_servers, logger)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _register_mcp_toolgroups_async(
|
|
59
|
+
client: AsyncLlamaStackAsLibraryClient,
|
|
60
|
+
mcp_servers: List[ModelContextProtocolServer],
|
|
61
|
+
logger: Logger,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Async logic for registering MCP toolgroups."""
|
|
64
|
+
# Get registered tools
|
|
65
|
+
registered_toolgroups = await client.toolgroups.list()
|
|
66
|
+
registered_toolgroups_ids = [
|
|
67
|
+
tool_group.provider_resource_id for tool_group in registered_toolgroups
|
|
68
|
+
]
|
|
69
|
+
logger.debug("Registered toolgroups: %s", registered_toolgroups_ids)
|
|
70
|
+
|
|
71
|
+
# Register toolgroups for MCP servers if not already registered
|
|
72
|
+
for mcp in mcp_servers:
|
|
73
|
+
if mcp.name not in registered_toolgroups_ids:
|
|
74
|
+
logger.debug("Registering MCP server: %s, %s", mcp.name, mcp.url)
|
|
75
|
+
|
|
76
|
+
registration_params = {
|
|
77
|
+
"toolgroup_id": mcp.name,
|
|
78
|
+
"provider_id": mcp.provider_id,
|
|
79
|
+
"mcp_endpoint": {"uri": mcp.url},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await client.toolgroups.register(**registration_params)
|
|
83
|
+
logger.debug("MCP server %s registered successfully", mcp.name)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _register_mcp_toolgroups_sync(
|
|
87
|
+
client: LlamaStackClient,
|
|
88
|
+
mcp_servers: List[ModelContextProtocolServer],
|
|
89
|
+
logger: Logger,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Sync logic for registering MCP toolgroups."""
|
|
92
|
+
# Get registered tool groups
|
|
93
|
+
registered_toolgroups = client.toolgroups.list()
|
|
94
|
+
registered_toolgroups_ids = [
|
|
95
|
+
tool_group.provider_resource_id for tool_group in registered_toolgroups
|
|
96
|
+
]
|
|
97
|
+
logger.debug("Registered toolgroups: %s", registered_toolgroups_ids)
|
|
98
|
+
|
|
99
|
+
# Register toolgroups for MCP servers if not already registered
|
|
100
|
+
for mcp in mcp_servers:
|
|
101
|
+
if mcp.name not in registered_toolgroups_ids:
|
|
102
|
+
logger.debug("Registering MCP server: %s, %s", mcp.name, mcp.url)
|
|
103
|
+
|
|
104
|
+
registration_params = {
|
|
105
|
+
"toolgroup_id": mcp.name,
|
|
106
|
+
"provider_id": mcp.provider_id,
|
|
107
|
+
"mcp_endpoint": {"uri": mcp.url},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
client.toolgroups.register(**registration_params)
|
|
111
|
+
logger.debug("MCP server %s registered successfully", mcp.name)
|
utils/endpoints.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Utility functions for endpoint handlers."""
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException, status
|
|
4
|
+
|
|
5
|
+
import constants
|
|
6
|
+
from models.requests import QueryRequest
|
|
7
|
+
from configuration import AppConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_configuration_loaded(configuration: AppConfig) -> None:
|
|
11
|
+
"""Check that configuration is loaded and raise exception when it is not."""
|
|
12
|
+
if configuration is None:
|
|
13
|
+
raise HTTPException(
|
|
14
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
15
|
+
detail={"response": "Configuration is not loaded"},
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_system_prompt(query_request: QueryRequest, configuration: AppConfig) -> str:
|
|
20
|
+
"""Get the system prompt: the provided one, configured one, or default one."""
|
|
21
|
+
# system prompt defined in query request has precendence
|
|
22
|
+
if query_request.system_prompt:
|
|
23
|
+
return query_request.system_prompt
|
|
24
|
+
|
|
25
|
+
# customized system prompt should be used when query request
|
|
26
|
+
# does not contain one
|
|
27
|
+
if (
|
|
28
|
+
configuration.customization is not None
|
|
29
|
+
and configuration.customization.system_prompt is not None
|
|
30
|
+
):
|
|
31
|
+
return configuration.customization.system_prompt
|
|
32
|
+
|
|
33
|
+
# default system prompt has the lowest precedence
|
|
34
|
+
return constants.DEFAULT_SYSTEM_PROMPT
|
utils/mcp_headers.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""MCP headers handling."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("app.endpoints.dependencies")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def mcp_headers_dependency(_request: Request) -> dict[str, dict[str, str]]:
|
|
11
|
+
"""Get the mcp headers dependency to passed to mcp servers.
|
|
12
|
+
|
|
13
|
+
mcp headers is a json dictionary or mcp url paths and their respective headers
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
request (Request): The FastAPI request object.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The mcp headers dictionary, or empty dictionary if not found or on json decoding error
|
|
20
|
+
"""
|
|
21
|
+
return extract_mcp_headers(_request)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def extract_mcp_headers(request: Request) -> dict[str, dict[str, str]]:
|
|
25
|
+
"""Extract mcp headers from MCP-HEADERS header.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
request: The FastAPI request object
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The mcp headers dictionary, or empty dictionary if not found or on json decoding error
|
|
32
|
+
"""
|
|
33
|
+
mcp_headers_string = request.headers.get("MCP-HEADERS", "")
|
|
34
|
+
mcp_headers = {}
|
|
35
|
+
if mcp_headers_string:
|
|
36
|
+
try:
|
|
37
|
+
mcp_headers = json.loads(mcp_headers_string)
|
|
38
|
+
except json.decoder.JSONDecodeError as e:
|
|
39
|
+
logger.error("MCP headers decode error: %s", e)
|
|
40
|
+
|
|
41
|
+
if not isinstance(mcp_headers, dict):
|
|
42
|
+
logger.error(
|
|
43
|
+
"MCP headers wrong type supplied (mcp headers must be a dictionary), "
|
|
44
|
+
"but type %s was supplied",
|
|
45
|
+
type(mcp_headers),
|
|
46
|
+
)
|
|
47
|
+
mcp_headers = {}
|
|
48
|
+
return mcp_headers
|
utils/suid.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Session ID utility functions."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_suid() -> str:
|
|
7
|
+
"""Generate a unique session ID (SUID) using UUID4.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
A unique session ID.
|
|
11
|
+
"""
|
|
12
|
+
return str(uuid.uuid4())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_suid(suid: str) -> bool:
|
|
16
|
+
"""Check if given string is a proper session ID.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
suid: The string to check.
|
|
20
|
+
|
|
21
|
+
Returns True if the string is a valid UUID, False otherwise.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
# accepts strings and bytes only
|
|
25
|
+
uuid.UUID(suid)
|
|
26
|
+
return True
|
|
27
|
+
except (ValueError, TypeError):
|
|
28
|
+
return False
|