mash-api 0.1.0__tar.gz
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.
- mash_api-0.1.0/PKG-INFO +47 -0
- mash_api-0.1.0/README.md +32 -0
- mash_api-0.1.0/pyproject.toml +30 -0
- mash_api-0.1.0/setup.cfg +4 -0
- mash_api-0.1.0/src/mash_api/__init__.py +7 -0
- mash_api-0.1.0/src/mash_api/app.py +662 -0
- mash_api-0.1.0/src/mash_api/config.py +53 -0
- mash_api-0.1.0/src/mash_api/main.py +114 -0
- mash_api-0.1.0/src/mash_api/types.py +25 -0
- mash_api-0.1.0/src/mash_api.egg-info/PKG-INFO +47 -0
- mash_api-0.1.0/src/mash_api.egg-info/SOURCES.txt +13 -0
- mash_api-0.1.0/src/mash_api.egg-info/dependency_links.txt +1 -0
- mash_api-0.1.0/src/mash_api.egg-info/entry_points.txt +2 -0
- mash_api-0.1.0/src/mash_api.egg-info/requires.txt +7 -0
- mash_api-0.1.0/src/mash_api.egg-info/top_level.txt +1 -0
mash_api-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mash-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenAPI service package for Mash applications
|
|
5
|
+
Author: imsid
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: mashpy>=0.1.2
|
|
10
|
+
Requires-Dist: fastapi>=0.115.0
|
|
11
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
14
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# mash-api
|
|
17
|
+
|
|
18
|
+
OpenAPI service package for self-hosted Mash applications.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install mash-api
|
|
24
|
+
# or
|
|
25
|
+
pip install "mashpy[api]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from mash_api import create_app
|
|
32
|
+
from my_app import definition
|
|
33
|
+
|
|
34
|
+
app = create_app(definition)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run with Uvicorn:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvicorn my_module:app --host 127.0.0.1 --port 8000
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or use the bundled CLI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
mash-api --app my_app:build_definition
|
|
47
|
+
```
|
mash_api-0.1.0/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# mash-api
|
|
2
|
+
|
|
3
|
+
OpenAPI service package for self-hosted Mash applications.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mash-api
|
|
9
|
+
# or
|
|
10
|
+
pip install "mashpy[api]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from mash_api import create_app
|
|
17
|
+
from my_app import definition
|
|
18
|
+
|
|
19
|
+
app = create_app(definition)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Run with Uvicorn:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uvicorn my_module:app --host 127.0.0.1 --port 8000
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or use the bundled CLI:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
mash-api --app my_app:build_definition
|
|
32
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mash-api"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "OpenAPI service package for Mash applications"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = {text = "Proprietary"}
|
|
8
|
+
authors = [{ name = "imsid" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"mashpy>=0.1.2",
|
|
11
|
+
"fastapi>=0.115.0",
|
|
12
|
+
"uvicorn>=0.30.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=8.3.0",
|
|
18
|
+
"httpx>=0.27.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
mash-api = "mash_api.main:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["setuptools>=68", "wheel"]
|
|
26
|
+
build-backend = "setuptools.build_meta"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
include = ["mash_api*"]
|
mash_api-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Public exports for mash-api package."""
|
|
2
|
+
|
|
3
|
+
from .app import create_app, run_app
|
|
4
|
+
from .config import MashAPIConfig
|
|
5
|
+
from .types import MashAPIAppSpec, SubagentRegistration
|
|
6
|
+
|
|
7
|
+
__all__ = ["create_app", "run_app", "MashAPIConfig", "MashAPIAppSpec", "SubagentRegistration"]
|
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"""FastAPI composition for mash-api runtime and telemetry endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, AsyncIterator, Iterator, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
import uvicorn
|
|
14
|
+
from fastapi import APIRouter, Depends, FastAPI, Query, Request
|
|
15
|
+
from fastapi.exceptions import RequestValidationError
|
|
16
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
17
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
from mash.logging.logger import EventLogger
|
|
21
|
+
from mash.memory.search.service import MemorySearchService
|
|
22
|
+
from mash.memory.search.types import FusionWeights, RetrievalConfig
|
|
23
|
+
from mash.memory.store import SQLiteStore
|
|
24
|
+
from mash.runtime import MashAgentClient, MashAgentClientError, MashAgentHost, MashRuntimeDefinition
|
|
25
|
+
|
|
26
|
+
from .config import MashAPIConfig
|
|
27
|
+
from .types import SubagentRegistration
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class APIError(RuntimeError):
|
|
31
|
+
"""Structured API error for envelope serialization."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
code: str,
|
|
37
|
+
message: str,
|
|
38
|
+
status_code: int,
|
|
39
|
+
details: Optional[dict[str, Any]] = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
self.code = code
|
|
43
|
+
self.message = message
|
|
44
|
+
self.status_code = status_code
|
|
45
|
+
self.details = details or {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class _AppRuntimeState:
|
|
50
|
+
host: MashAgentHost
|
|
51
|
+
primary_agent_id: str
|
|
52
|
+
primary_client: MashAgentClient
|
|
53
|
+
api_key: Optional[str]
|
|
54
|
+
observability_enabled: bool
|
|
55
|
+
observability_log_path: Optional[Path]
|
|
56
|
+
observability_memory_db_path: Optional[Path]
|
|
57
|
+
search_service: Optional[MemorySearchService]
|
|
58
|
+
default_events_limit: int
|
|
59
|
+
default_search_limit: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InvokeRequest(BaseModel):
|
|
63
|
+
message: str = Field(min_length=1)
|
|
64
|
+
session_id: Optional[str] = None
|
|
65
|
+
turn_metadata: dict[str, Any] = Field(default_factory=dict)
|
|
66
|
+
timeout_ms: Optional[int] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SubmitRequest(BaseModel):
|
|
70
|
+
message: str = Field(min_length=1)
|
|
71
|
+
session_id: Optional[str] = None
|
|
72
|
+
turn_metadata: dict[str, Any] = Field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PreferencesUpdateRequest(BaseModel):
|
|
76
|
+
preferences: dict[str, Any]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AppDataSetRequest(BaseModel):
|
|
80
|
+
value: Any
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CompactSessionRequest(BaseModel):
|
|
84
|
+
reason: str = "manual"
|
|
85
|
+
session_total_tokens_reset: int = 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _success(data: Any) -> dict[str, Any]:
|
|
89
|
+
return {"data": data}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _error_payload(code: str, message: str, details: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
|
93
|
+
return {
|
|
94
|
+
"error": {
|
|
95
|
+
"code": code,
|
|
96
|
+
"message": message,
|
|
97
|
+
"details": details or {},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _state_from_request(request: Request) -> _AppRuntimeState:
|
|
103
|
+
state = getattr(request.app.state, "runtime_state", None)
|
|
104
|
+
if state is None:
|
|
105
|
+
raise APIError(
|
|
106
|
+
code="RUNTIME_NOT_READY",
|
|
107
|
+
message="runtime is not initialized",
|
|
108
|
+
status_code=503,
|
|
109
|
+
)
|
|
110
|
+
return state
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _normalize_optional_text(value: Optional[str]) -> Optional[str]:
|
|
114
|
+
if value is None:
|
|
115
|
+
return None
|
|
116
|
+
text = value.strip()
|
|
117
|
+
return text or None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _require_message(value: str) -> str:
|
|
121
|
+
text = str(value or "").strip()
|
|
122
|
+
if not text:
|
|
123
|
+
raise APIError(code="INVALID_REQUEST", message="message is required", status_code=400)
|
|
124
|
+
return text
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_limit(raw: Optional[int], *, default: int, max_value: int) -> int:
|
|
128
|
+
if raw is None:
|
|
129
|
+
return default
|
|
130
|
+
return max(1, min(int(raw), max_value))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_runtime_event_sse_payload(event_name: str, payload: Any) -> str:
|
|
134
|
+
data = json.dumps(payload, ensure_ascii=True)
|
|
135
|
+
return f"event: {event_name}\ndata: {data}\n\n"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _build_observability_sse_payload(payload: str) -> str:
|
|
139
|
+
return f"data: {payload}\n\n"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def create_app(
|
|
143
|
+
definition: MashRuntimeDefinition,
|
|
144
|
+
*,
|
|
145
|
+
subagents: Sequence[SubagentRegistration] | None = None,
|
|
146
|
+
config: MashAPIConfig | None = None,
|
|
147
|
+
) -> FastAPI:
|
|
148
|
+
"""Build a FastAPI app that composes Mash runtime + observability APIs."""
|
|
149
|
+
|
|
150
|
+
resolved_config = config or MashAPIConfig()
|
|
151
|
+
|
|
152
|
+
@asynccontextmanager
|
|
153
|
+
async def _lifespan(application: FastAPI):
|
|
154
|
+
host = MashAgentHost(bind_host=resolved_config.runtime_bind_host)
|
|
155
|
+
primary_agent_id = host.register_primary(definition)
|
|
156
|
+
for registration in subagents or ():
|
|
157
|
+
host.register_subagent(
|
|
158
|
+
registration.definition,
|
|
159
|
+
metadata=registration.metadata,
|
|
160
|
+
agent_id=registration.agent_id,
|
|
161
|
+
)
|
|
162
|
+
host.start()
|
|
163
|
+
primary_client = host.get_client(primary_agent_id)
|
|
164
|
+
|
|
165
|
+
default_log_path = definition.get_log_destination().expanduser().resolve()
|
|
166
|
+
observability_log_path = resolved_config.resolved_log_path() or default_log_path
|
|
167
|
+
observability_memory_db_path = resolved_config.resolved_memory_db_path()
|
|
168
|
+
|
|
169
|
+
search_service: Optional[MemorySearchService] = None
|
|
170
|
+
if resolved_config.enable_observability and observability_memory_db_path is not None:
|
|
171
|
+
event_logger = EventLogger(observability_log_path)
|
|
172
|
+
search_service = MemorySearchService(
|
|
173
|
+
SQLiteStore(observability_memory_db_path),
|
|
174
|
+
event_logger=event_logger,
|
|
175
|
+
retrieval_config=RetrievalConfig(enable_keyword=True, enable_semantic=False),
|
|
176
|
+
fusion_weights=FusionWeights(keyword_weight=1.0, semantic_weight=0.0),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
application.state.runtime_state = _AppRuntimeState(
|
|
180
|
+
host=host,
|
|
181
|
+
primary_agent_id=primary_agent_id,
|
|
182
|
+
primary_client=primary_client,
|
|
183
|
+
api_key=resolved_config.resolved_api_key(),
|
|
184
|
+
observability_enabled=resolved_config.enable_observability,
|
|
185
|
+
observability_log_path=observability_log_path,
|
|
186
|
+
observability_memory_db_path=observability_memory_db_path,
|
|
187
|
+
search_service=search_service,
|
|
188
|
+
default_events_limit=max(1, int(resolved_config.default_events_limit)),
|
|
189
|
+
default_search_limit=max(1, int(resolved_config.default_search_limit)),
|
|
190
|
+
)
|
|
191
|
+
try:
|
|
192
|
+
yield
|
|
193
|
+
finally:
|
|
194
|
+
state = getattr(application.state, "runtime_state", None)
|
|
195
|
+
if state is not None:
|
|
196
|
+
state.host.close()
|
|
197
|
+
application.state.runtime_state = None
|
|
198
|
+
|
|
199
|
+
app = FastAPI(title="Mash API", version="1.0.0", lifespan=_lifespan)
|
|
200
|
+
|
|
201
|
+
cors_origins = resolved_config.resolved_cors_origins()
|
|
202
|
+
if cors_origins:
|
|
203
|
+
app.add_middleware(
|
|
204
|
+
CORSMiddleware,
|
|
205
|
+
allow_origins=cors_origins,
|
|
206
|
+
allow_methods=["*"],
|
|
207
|
+
allow_headers=["*"],
|
|
208
|
+
allow_credentials=False,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@app.exception_handler(APIError)
|
|
212
|
+
async def _api_error_handler(_: Request, exc: APIError) -> JSONResponse:
|
|
213
|
+
return JSONResponse(
|
|
214
|
+
status_code=exc.status_code,
|
|
215
|
+
content=_error_payload(exc.code, exc.message, exc.details),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@app.exception_handler(RequestValidationError)
|
|
219
|
+
async def _validation_error_handler(_: Request, exc: RequestValidationError) -> JSONResponse:
|
|
220
|
+
return JSONResponse(
|
|
221
|
+
status_code=422,
|
|
222
|
+
content=_error_payload(
|
|
223
|
+
"VALIDATION_ERROR",
|
|
224
|
+
"request validation failed",
|
|
225
|
+
{"errors": exc.errors()},
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@app.exception_handler(MashAgentClientError)
|
|
230
|
+
async def _client_error_handler(_: Request, exc: MashAgentClientError) -> JSONResponse:
|
|
231
|
+
return JSONResponse(
|
|
232
|
+
status_code=502,
|
|
233
|
+
content=_error_payload("RUNTIME_CLIENT_ERROR", str(exc)),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async def _authorize(request: Request) -> None:
|
|
237
|
+
state = _state_from_request(request)
|
|
238
|
+
expected_key = state.api_key
|
|
239
|
+
if expected_key is None:
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
header_token: Optional[str] = None
|
|
243
|
+
auth_header = request.headers.get("authorization")
|
|
244
|
+
if isinstance(auth_header, str) and auth_header.lower().startswith("bearer "):
|
|
245
|
+
header_token = auth_header[7:].strip() or None
|
|
246
|
+
|
|
247
|
+
x_api_key = _normalize_optional_text(request.headers.get("x-api-key"))
|
|
248
|
+
provided = header_token or x_api_key
|
|
249
|
+
if provided != expected_key:
|
|
250
|
+
raise APIError(
|
|
251
|
+
code="UNAUTHORIZED",
|
|
252
|
+
message="valid API key is required",
|
|
253
|
+
status_code=401,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
api = APIRouter(prefix=resolved_config.api_prefix, dependencies=[Depends(_authorize)])
|
|
257
|
+
|
|
258
|
+
@api.get("/health")
|
|
259
|
+
def health(request: Request) -> dict[str, Any]:
|
|
260
|
+
state = _state_from_request(request)
|
|
261
|
+
session_info = state.primary_client.get_session_info()
|
|
262
|
+
log_path = state.observability_log_path
|
|
263
|
+
return _success(
|
|
264
|
+
{
|
|
265
|
+
"status": "ok",
|
|
266
|
+
"api_version": "v1",
|
|
267
|
+
"runtime": {
|
|
268
|
+
"primary_agent_id": state.primary_agent_id,
|
|
269
|
+
"app_id": session_info.get("app_id"),
|
|
270
|
+
"session_id": session_info.get("session_id"),
|
|
271
|
+
"subagent_ids": session_info.get("subagent_ids", []),
|
|
272
|
+
"model": session_info.get("model"),
|
|
273
|
+
"max_steps": session_info.get("max_steps"),
|
|
274
|
+
},
|
|
275
|
+
"observability": {
|
|
276
|
+
"enabled": state.observability_enabled,
|
|
277
|
+
"events": {
|
|
278
|
+
"configured": log_path is not None,
|
|
279
|
+
"path": str(log_path) if log_path is not None else None,
|
|
280
|
+
"exists": bool(log_path.exists()) if log_path is not None else False,
|
|
281
|
+
"default_limit": state.default_events_limit,
|
|
282
|
+
},
|
|
283
|
+
"memory": {
|
|
284
|
+
"configured": state.observability_memory_db_path is not None,
|
|
285
|
+
"search_available": state.search_service is not None,
|
|
286
|
+
"path": (
|
|
287
|
+
str(state.observability_memory_db_path)
|
|
288
|
+
if state.observability_memory_db_path is not None
|
|
289
|
+
else None
|
|
290
|
+
),
|
|
291
|
+
"default_limit": state.default_search_limit,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
@api.post("/interactions/invoke")
|
|
298
|
+
def invoke(request: Request, body: InvokeRequest) -> dict[str, Any]:
|
|
299
|
+
state = _state_from_request(request)
|
|
300
|
+
message = _require_message(body.message)
|
|
301
|
+
session_id = _normalize_optional_text(body.session_id)
|
|
302
|
+
timeout_ms = body.timeout_ms if body.timeout_ms is None else int(body.timeout_ms)
|
|
303
|
+
if timeout_ms is not None and timeout_ms <= 0:
|
|
304
|
+
timeout_ms = None
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
result = state.primary_client.invoke(
|
|
308
|
+
message,
|
|
309
|
+
session_id=session_id,
|
|
310
|
+
turn_metadata=dict(body.turn_metadata or {}),
|
|
311
|
+
timeout_ms=timeout_ms,
|
|
312
|
+
)
|
|
313
|
+
except TimeoutError as exc:
|
|
314
|
+
raise APIError(
|
|
315
|
+
code="REQUEST_TIMEOUT",
|
|
316
|
+
message=str(exc),
|
|
317
|
+
status_code=504,
|
|
318
|
+
) from exc
|
|
319
|
+
return _success(result)
|
|
320
|
+
|
|
321
|
+
@api.post("/interactions/requests")
|
|
322
|
+
def submit_request(request: Request, body: SubmitRequest) -> dict[str, Any]:
|
|
323
|
+
state = _state_from_request(request)
|
|
324
|
+
request_id = state.primary_client.post_request(
|
|
325
|
+
_require_message(body.message),
|
|
326
|
+
session_id=_normalize_optional_text(body.session_id),
|
|
327
|
+
turn_metadata=dict(body.turn_metadata or {}),
|
|
328
|
+
)
|
|
329
|
+
return _success({"request_id": request_id})
|
|
330
|
+
|
|
331
|
+
@api.get("/interactions/requests/{request_id}/events")
|
|
332
|
+
def stream_request_events(request: Request, request_id: str) -> StreamingResponse:
|
|
333
|
+
state = _state_from_request(request)
|
|
334
|
+
normalized_request_id = request_id.strip()
|
|
335
|
+
if not normalized_request_id:
|
|
336
|
+
raise APIError(
|
|
337
|
+
code="INVALID_REQUEST",
|
|
338
|
+
message="request_id is required",
|
|
339
|
+
status_code=400,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _generate() -> Iterator[str]:
|
|
343
|
+
try:
|
|
344
|
+
for event in state.primary_client.stream(normalized_request_id):
|
|
345
|
+
event_name = str(event.get("event") or "message")
|
|
346
|
+
payload = event.get("data")
|
|
347
|
+
yield _build_runtime_event_sse_payload(event_name, payload)
|
|
348
|
+
if event_name in {"request.completed", "request.error"}:
|
|
349
|
+
break
|
|
350
|
+
except MashAgentClientError as exc:
|
|
351
|
+
yield _build_runtime_event_sse_payload(
|
|
352
|
+
"request.error",
|
|
353
|
+
{
|
|
354
|
+
"request_id": normalized_request_id,
|
|
355
|
+
"status": "error",
|
|
356
|
+
"error": str(exc),
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return StreamingResponse(
|
|
361
|
+
_generate(),
|
|
362
|
+
media_type="text/event-stream",
|
|
363
|
+
headers={
|
|
364
|
+
"Cache-Control": "no-cache",
|
|
365
|
+
"Connection": "keep-alive",
|
|
366
|
+
},
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@api.get("/runtime/session")
|
|
370
|
+
def get_runtime_session(request: Request, session_id: Optional[str] = None) -> dict[str, Any]:
|
|
371
|
+
state = _state_from_request(request)
|
|
372
|
+
info = state.primary_client.get_session_info(_normalize_optional_text(session_id))
|
|
373
|
+
return _success(info)
|
|
374
|
+
|
|
375
|
+
@api.get("/runtime/subagents")
|
|
376
|
+
def get_subagents(request: Request) -> dict[str, Any]:
|
|
377
|
+
state = _state_from_request(request)
|
|
378
|
+
return _success({"subagent_ids": state.primary_client.get_subagent_ids()})
|
|
379
|
+
|
|
380
|
+
@api.get("/runtime/sessions/{session_id}/preferences")
|
|
381
|
+
def get_preferences(request: Request, session_id: str) -> dict[str, Any]:
|
|
382
|
+
state = _state_from_request(request)
|
|
383
|
+
if not session_id.strip():
|
|
384
|
+
raise APIError(code="INVALID_REQUEST", message="session_id is required", status_code=400)
|
|
385
|
+
return _success({"preferences": state.primary_client.get_preferences(session_id)})
|
|
386
|
+
|
|
387
|
+
@api.put("/runtime/sessions/{session_id}/preferences")
|
|
388
|
+
def set_preferences(request: Request, session_id: str, body: PreferencesUpdateRequest) -> dict[str, Any]:
|
|
389
|
+
state = _state_from_request(request)
|
|
390
|
+
if not session_id.strip():
|
|
391
|
+
raise APIError(code="INVALID_REQUEST", message="session_id is required", status_code=400)
|
|
392
|
+
state.primary_client.set_preferences(session_id, body.preferences)
|
|
393
|
+
return _success({"ok": True})
|
|
394
|
+
|
|
395
|
+
@api.get("/runtime/sessions/{session_id}/app-data")
|
|
396
|
+
def list_app_data(request: Request, session_id: str) -> dict[str, Any]:
|
|
397
|
+
state = _state_from_request(request)
|
|
398
|
+
if not session_id.strip():
|
|
399
|
+
raise APIError(code="INVALID_REQUEST", message="session_id is required", status_code=400)
|
|
400
|
+
return _success({"items": state.primary_client.list_app_data(session_id)})
|
|
401
|
+
|
|
402
|
+
@api.get("/runtime/sessions/{session_id}/app-data/{key}")
|
|
403
|
+
def get_app_data(request: Request, session_id: str, key: str) -> dict[str, Any]:
|
|
404
|
+
state = _state_from_request(request)
|
|
405
|
+
if not session_id.strip() or not key.strip():
|
|
406
|
+
raise APIError(code="INVALID_REQUEST", message="session_id and key are required", status_code=400)
|
|
407
|
+
return _success({"value": state.primary_client.get_app_data(session_id, key)})
|
|
408
|
+
|
|
409
|
+
@api.put("/runtime/sessions/{session_id}/app-data/{key}")
|
|
410
|
+
def set_app_data(request: Request, session_id: str, key: str, body: AppDataSetRequest) -> dict[str, Any]:
|
|
411
|
+
state = _state_from_request(request)
|
|
412
|
+
if not session_id.strip() or not key.strip():
|
|
413
|
+
raise APIError(code="INVALID_REQUEST", message="session_id and key are required", status_code=400)
|
|
414
|
+
state.primary_client.set_app_data(session_id, key, body.value)
|
|
415
|
+
return _success({"ok": True})
|
|
416
|
+
|
|
417
|
+
@api.delete("/runtime/sessions/{session_id}/app-data/{key}")
|
|
418
|
+
def delete_app_data(request: Request, session_id: str, key: str) -> dict[str, Any]:
|
|
419
|
+
state = _state_from_request(request)
|
|
420
|
+
if not session_id.strip() or not key.strip():
|
|
421
|
+
raise APIError(code="INVALID_REQUEST", message="session_id and key are required", status_code=400)
|
|
422
|
+
deleted = state.primary_client.delete_app_data(session_id, key)
|
|
423
|
+
return _success({"deleted": bool(deleted)})
|
|
424
|
+
|
|
425
|
+
@api.get("/runtime/sessions/{session_id}/history")
|
|
426
|
+
def get_history(
|
|
427
|
+
request: Request,
|
|
428
|
+
session_id: str,
|
|
429
|
+
limit: Optional[int] = Query(default=None),
|
|
430
|
+
) -> dict[str, Any]:
|
|
431
|
+
state = _state_from_request(request)
|
|
432
|
+
if not session_id.strip():
|
|
433
|
+
raise APIError(code="INVALID_REQUEST", message="session_id is required", status_code=400)
|
|
434
|
+
turns = state.primary_client.get_history_turns(session_id, limit=limit)
|
|
435
|
+
return _success({"turns": turns})
|
|
436
|
+
|
|
437
|
+
@api.post("/runtime/sessions/{session_id}/compact")
|
|
438
|
+
def compact_session(
|
|
439
|
+
request: Request,
|
|
440
|
+
session_id: str,
|
|
441
|
+
body: CompactSessionRequest,
|
|
442
|
+
) -> dict[str, Any]:
|
|
443
|
+
state = _state_from_request(request)
|
|
444
|
+
if not session_id.strip():
|
|
445
|
+
raise APIError(code="INVALID_REQUEST", message="session_id is required", status_code=400)
|
|
446
|
+
summary_text, turn_id = state.primary_client.compact_session(
|
|
447
|
+
session_id=session_id,
|
|
448
|
+
reason=body.reason,
|
|
449
|
+
session_total_tokens_reset=body.session_total_tokens_reset,
|
|
450
|
+
)
|
|
451
|
+
return _success({"summary_text": summary_text, "turn_id": turn_id})
|
|
452
|
+
|
|
453
|
+
@api.get("/telemetry/events")
|
|
454
|
+
def get_observability_events(
|
|
455
|
+
request: Request,
|
|
456
|
+
limit: Optional[int] = Query(default=None),
|
|
457
|
+
) -> dict[str, Any]:
|
|
458
|
+
state = _state_from_request(request)
|
|
459
|
+
if not state.observability_enabled:
|
|
460
|
+
raise APIError(
|
|
461
|
+
code="OBSERVABILITY_DISABLED",
|
|
462
|
+
message="telemetry endpoints are disabled",
|
|
463
|
+
status_code=503,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
log_path = state.observability_log_path
|
|
467
|
+
if log_path is None:
|
|
468
|
+
raise APIError(
|
|
469
|
+
code="OBSERVABILITY_EVENTS_UNCONFIGURED",
|
|
470
|
+
message="telemetry log path is not configured",
|
|
471
|
+
status_code=503,
|
|
472
|
+
)
|
|
473
|
+
if not log_path.exists():
|
|
474
|
+
raise APIError(
|
|
475
|
+
code="LOG_FILE_NOT_FOUND",
|
|
476
|
+
message="log file not found",
|
|
477
|
+
status_code=404,
|
|
478
|
+
details={"path": str(log_path)},
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
resolved_limit = _parse_limit(limit, default=state.default_events_limit, max_value=20000)
|
|
482
|
+
events: list[dict[str, Any]] = []
|
|
483
|
+
with log_path.open("r", encoding="utf-8") as handle:
|
|
484
|
+
lines = handle.readlines()
|
|
485
|
+
|
|
486
|
+
for raw in lines[-resolved_limit:]:
|
|
487
|
+
line = raw.strip()
|
|
488
|
+
if not line:
|
|
489
|
+
continue
|
|
490
|
+
try:
|
|
491
|
+
payload = json.loads(line)
|
|
492
|
+
except json.JSONDecodeError:
|
|
493
|
+
continue
|
|
494
|
+
if isinstance(payload, dict):
|
|
495
|
+
events.append(payload)
|
|
496
|
+
|
|
497
|
+
return _success({
|
|
498
|
+
"events": events,
|
|
499
|
+
"path": str(log_path),
|
|
500
|
+
"limit": resolved_limit,
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
@api.get("/telemetry/events/stream")
|
|
504
|
+
async def stream_observability_events(request: Request) -> StreamingResponse:
|
|
505
|
+
state = _state_from_request(request)
|
|
506
|
+
if not state.observability_enabled:
|
|
507
|
+
raise APIError(
|
|
508
|
+
code="OBSERVABILITY_DISABLED",
|
|
509
|
+
message="telemetry endpoints are disabled",
|
|
510
|
+
status_code=503,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
log_path = state.observability_log_path
|
|
514
|
+
if log_path is None:
|
|
515
|
+
raise APIError(
|
|
516
|
+
code="OBSERVABILITY_EVENTS_UNCONFIGURED",
|
|
517
|
+
message="telemetry log path is not configured",
|
|
518
|
+
status_code=503,
|
|
519
|
+
)
|
|
520
|
+
if not log_path.exists():
|
|
521
|
+
raise APIError(
|
|
522
|
+
code="LOG_FILE_NOT_FOUND",
|
|
523
|
+
message="log file not found",
|
|
524
|
+
status_code=404,
|
|
525
|
+
details={"path": str(log_path)},
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
async def _generate() -> AsyncIterator[str]:
|
|
529
|
+
with log_path.open("r", encoding="utf-8") as handle:
|
|
530
|
+
handle.seek(0, os.SEEK_END)
|
|
531
|
+
while True:
|
|
532
|
+
if await request.is_disconnected():
|
|
533
|
+
break
|
|
534
|
+
line = handle.readline()
|
|
535
|
+
if not line:
|
|
536
|
+
yield ": keep-alive\n\n"
|
|
537
|
+
await asyncio.sleep(0.25)
|
|
538
|
+
continue
|
|
539
|
+
payload = line.strip()
|
|
540
|
+
if not payload:
|
|
541
|
+
continue
|
|
542
|
+
try:
|
|
543
|
+
json.loads(payload)
|
|
544
|
+
except json.JSONDecodeError:
|
|
545
|
+
continue
|
|
546
|
+
yield _build_observability_sse_payload(payload)
|
|
547
|
+
|
|
548
|
+
return StreamingResponse(
|
|
549
|
+
_generate(),
|
|
550
|
+
media_type="text/event-stream",
|
|
551
|
+
headers={
|
|
552
|
+
"Cache-Control": "no-cache",
|
|
553
|
+
"Connection": "keep-alive",
|
|
554
|
+
},
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
@api.get("/telemetry/memory/search")
|
|
558
|
+
def search_memory(
|
|
559
|
+
request: Request,
|
|
560
|
+
q: str,
|
|
561
|
+
app_id: str,
|
|
562
|
+
session_id: Optional[str] = None,
|
|
563
|
+
limit: Optional[int] = Query(default=None),
|
|
564
|
+
) -> dict[str, Any]:
|
|
565
|
+
state = _state_from_request(request)
|
|
566
|
+
if not state.observability_enabled:
|
|
567
|
+
raise APIError(
|
|
568
|
+
code="OBSERVABILITY_DISABLED",
|
|
569
|
+
message="telemetry endpoints are disabled",
|
|
570
|
+
status_code=503,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
query_text = q.strip()
|
|
574
|
+
if not query_text:
|
|
575
|
+
raise APIError(
|
|
576
|
+
code="MISSING_QUERY",
|
|
577
|
+
message="q is required",
|
|
578
|
+
status_code=400,
|
|
579
|
+
details={"param": "q"},
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
app_id_value = app_id.strip()
|
|
583
|
+
if not app_id_value:
|
|
584
|
+
raise APIError(
|
|
585
|
+
code="MISSING_APP_ID",
|
|
586
|
+
message="app_id is required",
|
|
587
|
+
status_code=400,
|
|
588
|
+
details={"param": "app_id"},
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
if state.search_service is None:
|
|
592
|
+
raise APIError(
|
|
593
|
+
code="MEMORY_SEARCH_UNAVAILABLE",
|
|
594
|
+
message="memory search unavailable (configure memory db path)",
|
|
595
|
+
status_code=503,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
resolved_limit = _parse_limit(limit, default=state.default_search_limit, max_value=50)
|
|
599
|
+
normalized_session_id = _normalize_optional_text(session_id)
|
|
600
|
+
try:
|
|
601
|
+
results = state.search_service.search(
|
|
602
|
+
query_text,
|
|
603
|
+
limit=resolved_limit,
|
|
604
|
+
session_id=normalized_session_id,
|
|
605
|
+
app_id=app_id_value,
|
|
606
|
+
)
|
|
607
|
+
except ValueError as exc:
|
|
608
|
+
raise APIError(
|
|
609
|
+
code="SEARCH_VALIDATION_ERROR",
|
|
610
|
+
message=str(exc),
|
|
611
|
+
status_code=400,
|
|
612
|
+
) from exc
|
|
613
|
+
except (NotImplementedError, RuntimeError) as exc:
|
|
614
|
+
raise APIError(
|
|
615
|
+
code="SEARCH_UNAVAILABLE",
|
|
616
|
+
message=str(exc),
|
|
617
|
+
status_code=503,
|
|
618
|
+
) from exc
|
|
619
|
+
except Exception as exc: # pragma: no cover
|
|
620
|
+
raise APIError(
|
|
621
|
+
code="SEARCH_FAILED",
|
|
622
|
+
message=f"search failed: {exc}",
|
|
623
|
+
status_code=500,
|
|
624
|
+
) from exc
|
|
625
|
+
|
|
626
|
+
return _success(
|
|
627
|
+
{
|
|
628
|
+
"results": [asdict(result) for result in results],
|
|
629
|
+
"app_id": app_id_value,
|
|
630
|
+
"session_id": normalized_session_id,
|
|
631
|
+
"query": query_text,
|
|
632
|
+
"limit": resolved_limit,
|
|
633
|
+
}
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
app.include_router(api)
|
|
637
|
+
|
|
638
|
+
@app.get("/")
|
|
639
|
+
def root() -> dict[str, Any]:
|
|
640
|
+
return {
|
|
641
|
+
"service": "mash-api",
|
|
642
|
+
"api": {
|
|
643
|
+
"version": "v1",
|
|
644
|
+
"base": resolved_config.api_prefix,
|
|
645
|
+
"openapi": "/openapi.json",
|
|
646
|
+
"docs": "/docs",
|
|
647
|
+
},
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return app
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def run_app(
|
|
654
|
+
definition: MashRuntimeDefinition,
|
|
655
|
+
*,
|
|
656
|
+
subagents: Sequence[SubagentRegistration] | None = None,
|
|
657
|
+
config: MashAPIConfig | None = None,
|
|
658
|
+
) -> None:
|
|
659
|
+
"""Run mash-api service with uvicorn."""
|
|
660
|
+
resolved_config = config or MashAPIConfig()
|
|
661
|
+
app = create_app(definition, subagents=subagents, config=resolved_config)
|
|
662
|
+
uvicorn.run(app, host=resolved_config.bind_host, port=resolved_config.bind_port)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Configuration types for mash-api."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Sequence
|
|
8
|
+
|
|
9
|
+
_DEFAULT_CORS_ORIGINS = (
|
|
10
|
+
"http://127.0.0.1:3000",
|
|
11
|
+
"http://localhost:3000",
|
|
12
|
+
"http://127.0.0.1:5173",
|
|
13
|
+
"http://localhost:5173",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class MashAPIConfig:
|
|
19
|
+
"""Runtime configuration for mash-api server composition."""
|
|
20
|
+
|
|
21
|
+
api_prefix: str = "/api/v1"
|
|
22
|
+
bind_host: str = "127.0.0.1"
|
|
23
|
+
bind_port: int = 8000
|
|
24
|
+
runtime_bind_host: str = "127.0.0.1"
|
|
25
|
+
api_key: Optional[str] = None
|
|
26
|
+
cors_allow_origins: Sequence[str] = field(default_factory=lambda: _DEFAULT_CORS_ORIGINS)
|
|
27
|
+
enable_observability: bool = True
|
|
28
|
+
observability_log_path: Optional[Path] = None
|
|
29
|
+
observability_memory_db_path: Optional[Path] = None
|
|
30
|
+
default_events_limit: int = 2000
|
|
31
|
+
default_search_limit: int = 10
|
|
32
|
+
|
|
33
|
+
def resolved_api_key(self) -> Optional[str]:
|
|
34
|
+
value = (self.api_key or "").strip()
|
|
35
|
+
return value or None
|
|
36
|
+
|
|
37
|
+
def resolved_cors_origins(self) -> list[str]:
|
|
38
|
+
values: list[str] = []
|
|
39
|
+
for origin in self.cors_allow_origins:
|
|
40
|
+
text = str(origin).strip()
|
|
41
|
+
if text:
|
|
42
|
+
values.append(text)
|
|
43
|
+
return values
|
|
44
|
+
|
|
45
|
+
def resolved_log_path(self) -> Optional[Path]:
|
|
46
|
+
if self.observability_log_path is None:
|
|
47
|
+
return None
|
|
48
|
+
return Path(self.observability_log_path).expanduser().resolve()
|
|
49
|
+
|
|
50
|
+
def resolved_memory_db_path(self) -> Optional[Path]:
|
|
51
|
+
if self.observability_memory_db_path is None:
|
|
52
|
+
return None
|
|
53
|
+
return Path(self.observability_memory_db_path).expanduser().resolve()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""CLI entrypoint for mash-api package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Sequence
|
|
9
|
+
|
|
10
|
+
import uvicorn
|
|
11
|
+
|
|
12
|
+
from mash.runtime import MashRuntimeDefinition
|
|
13
|
+
|
|
14
|
+
from .app import create_app
|
|
15
|
+
from .config import MashAPIConfig
|
|
16
|
+
from .types import MashAPIAppSpec
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_target(app_ref: str) -> Any:
|
|
20
|
+
module_name, sep, attr_name = app_ref.partition(":")
|
|
21
|
+
if not sep or not module_name.strip() or not attr_name.strip():
|
|
22
|
+
raise ValueError("--app must be in 'module:attribute' format")
|
|
23
|
+
|
|
24
|
+
module = importlib.import_module(module_name)
|
|
25
|
+
if not hasattr(module, attr_name):
|
|
26
|
+
raise ValueError(f"module '{module_name}' has no attribute '{attr_name}'")
|
|
27
|
+
return getattr(module, attr_name)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_spec(app_ref: str) -> MashAPIAppSpec:
|
|
31
|
+
target = _load_target(app_ref)
|
|
32
|
+
resolved = target() if callable(target) else target
|
|
33
|
+
|
|
34
|
+
if isinstance(resolved, MashAPIAppSpec):
|
|
35
|
+
return resolved
|
|
36
|
+
if isinstance(resolved, MashRuntimeDefinition):
|
|
37
|
+
return MashAPIAppSpec(definition=resolved)
|
|
38
|
+
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"app target must resolve to MashRuntimeDefinition or MashAPIAppSpec"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
45
|
+
parser = argparse.ArgumentParser(description="Run mash-api server")
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--app",
|
|
48
|
+
required=True,
|
|
49
|
+
help="Application reference in module:attribute format.",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument("--host", default="127.0.0.1", help="API bind host")
|
|
52
|
+
parser.add_argument("--port", type=int, default=8000, help="API bind port")
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--runtime-bind-host",
|
|
55
|
+
default="127.0.0.1",
|
|
56
|
+
help="Internal mash runtime bind host",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--api-key",
|
|
60
|
+
default=None,
|
|
61
|
+
help="Optional API key (or set MASH_API_KEY)",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--cors-origin",
|
|
65
|
+
action="append",
|
|
66
|
+
default=None,
|
|
67
|
+
help="Allowed CORS origin; repeat for multiple values.",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument("--log-path", default=None, help="Observability log path")
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--memory-db",
|
|
72
|
+
default=None,
|
|
73
|
+
help="Optional SQLite path for /telemetry/memory/search",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--disable-observability",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="Disable telemetry endpoints.",
|
|
79
|
+
)
|
|
80
|
+
return parser
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
84
|
+
parser = build_parser()
|
|
85
|
+
args = parser.parse_args(argv)
|
|
86
|
+
|
|
87
|
+
spec = _resolve_spec(args.app)
|
|
88
|
+
|
|
89
|
+
api_key = args.api_key
|
|
90
|
+
if api_key is None:
|
|
91
|
+
api_key = os.environ.get("MASH_API_KEY")
|
|
92
|
+
|
|
93
|
+
config = MashAPIConfig(
|
|
94
|
+
bind_host=args.host,
|
|
95
|
+
bind_port=args.port,
|
|
96
|
+
runtime_bind_host=args.runtime_bind_host,
|
|
97
|
+
api_key=api_key,
|
|
98
|
+
cors_allow_origins=(
|
|
99
|
+
args.cors_origin
|
|
100
|
+
if args.cors_origin is not None
|
|
101
|
+
else MashAPIConfig().cors_allow_origins
|
|
102
|
+
),
|
|
103
|
+
enable_observability=not args.disable_observability,
|
|
104
|
+
observability_log_path=args.log_path,
|
|
105
|
+
observability_memory_db_path=args.memory_db,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
app = create_app(spec.definition, subagents=spec.subagents, config=config)
|
|
109
|
+
uvicorn.run(app, host=config.bind_host, port=config.bind_port)
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Public types for mash-api app composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from mash.runtime import MashRuntimeDefinition, SubAgentMetadata
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class SubagentRegistration:
|
|
13
|
+
"""Subagent registration payload for host-backed API composition."""
|
|
14
|
+
|
|
15
|
+
definition: MashRuntimeDefinition
|
|
16
|
+
metadata: SubAgentMetadata
|
|
17
|
+
agent_id: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class MashAPIAppSpec:
|
|
22
|
+
"""Application spec used by mash-api CLI loading."""
|
|
23
|
+
|
|
24
|
+
definition: MashRuntimeDefinition
|
|
25
|
+
subagents: Sequence[SubagentRegistration] = ()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mash-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenAPI service package for Mash applications
|
|
5
|
+
Author: imsid
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: mashpy>=0.1.2
|
|
10
|
+
Requires-Dist: fastapi>=0.115.0
|
|
11
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
14
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# mash-api
|
|
17
|
+
|
|
18
|
+
OpenAPI service package for self-hosted Mash applications.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install mash-api
|
|
24
|
+
# or
|
|
25
|
+
pip install "mashpy[api]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from mash_api import create_app
|
|
32
|
+
from my_app import definition
|
|
33
|
+
|
|
34
|
+
app = create_app(definition)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Run with Uvicorn:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvicorn my_module:app --host 127.0.0.1 --port 8000
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or use the bundled CLI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
mash-api --app my_app:build_definition
|
|
47
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/mash_api/__init__.py
|
|
4
|
+
src/mash_api/app.py
|
|
5
|
+
src/mash_api/config.py
|
|
6
|
+
src/mash_api/main.py
|
|
7
|
+
src/mash_api/types.py
|
|
8
|
+
src/mash_api.egg-info/PKG-INFO
|
|
9
|
+
src/mash_api.egg-info/SOURCES.txt
|
|
10
|
+
src/mash_api.egg-info/dependency_links.txt
|
|
11
|
+
src/mash_api.egg-info/entry_points.txt
|
|
12
|
+
src/mash_api.egg-info/requires.txt
|
|
13
|
+
src/mash_api.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mash_api
|