general-augment-cli 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.
- general_augment_cli-0.1.0.dist-info/METADATA +180 -0
- general_augment_cli-0.1.0.dist-info/RECORD +42 -0
- general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
- general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
- platform_cli/__init__.py +5 -0
- platform_cli/branding.py +27 -0
- platform_cli/client.py +179 -0
- platform_cli/commands/__init__.py +1 -0
- platform_cli/commands/approvals.py +150 -0
- platform_cli/commands/auth.py +96 -0
- platform_cli/commands/billing.py +143 -0
- platform_cli/commands/channels.py +212 -0
- platform_cli/commands/deploy.py +72 -0
- platform_cli/commands/dev.py +38 -0
- platform_cli/commands/doctor.py +170 -0
- platform_cli/commands/identity.py +433 -0
- platform_cli/commands/init.py +55 -0
- platform_cli/commands/integrate.py +94 -0
- platform_cli/commands/keys.py +116 -0
- platform_cli/commands/logs.py +43 -0
- platform_cli/commands/mcp.py +258 -0
- platform_cli/commands/memory.py +316 -0
- platform_cli/commands/mock.py +30 -0
- platform_cli/commands/model_providers.py +226 -0
- platform_cli/commands/observability.py +174 -0
- platform_cli/commands/onboarding.py +72 -0
- platform_cli/commands/projects.py +302 -0
- platform_cli/commands/skills.py +116 -0
- platform_cli/commands/smoke.py +280 -0
- platform_cli/commands/status.py +49 -0
- platform_cli/commands/tools.py +179 -0
- platform_cli/commands/users.py +150 -0
- platform_cli/commands/validate.py +96 -0
- platform_cli/commands/verify.py +648 -0
- platform_cli/config.py +114 -0
- platform_cli/errors.py +103 -0
- platform_cli/local_mock.py +1392 -0
- platform_cli/main.py +130 -0
- platform_cli/openapi.py +1048 -0
- platform_cli/output.py +47 -0
- platform_cli/readiness.py +176 -0
- platform_cli/runtime.py +22 -0
|
@@ -0,0 +1,1392 @@
|
|
|
1
|
+
"""Run a local test-only General Augment HTTP mock server.
|
|
2
|
+
|
|
3
|
+
The mock is for app contract tests and fixtures. It does not run Hermes, call models,
|
|
4
|
+
enforce billing, validate credentials, or persist data outside this process.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import copy
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from http import HTTPStatus
|
|
18
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
19
|
+
from typing import Any, cast
|
|
20
|
+
from urllib.parse import parse_qs, unquote, urlparse
|
|
21
|
+
from uuid import NAMESPACE_URL, uuid5
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
26
|
+
DEFAULT_PORT = 8787
|
|
27
|
+
MOCK_CREATED_AT = "2026-01-01T00:00:00Z"
|
|
28
|
+
EXACT_REPLY_RE = re.compile(r"reply exactly with:\s*(.+)", re.IGNORECASE)
|
|
29
|
+
HEALTH_PATHS = {"/v1/health", "/health/ready", "/health/live"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class MemoryFact:
|
|
34
|
+
"""One in-memory fact stored by the local mock."""
|
|
35
|
+
|
|
36
|
+
memory_id: str
|
|
37
|
+
user_id: str
|
|
38
|
+
fact: str
|
|
39
|
+
fact_type: str = "fact"
|
|
40
|
+
importance_score: float | None = None
|
|
41
|
+
source: str | None = None
|
|
42
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
created_at: str = MOCK_CREATED_AT
|
|
44
|
+
expires_at: str | None = None
|
|
45
|
+
|
|
46
|
+
def as_result(self, score: float | None) -> dict[str, Any]:
|
|
47
|
+
"""Return a public memory-hit shape."""
|
|
48
|
+
return {
|
|
49
|
+
"id": self.memory_id,
|
|
50
|
+
"memory_id": self.memory_id,
|
|
51
|
+
"fact": self.fact,
|
|
52
|
+
"fact_type": self.fact_type,
|
|
53
|
+
"content": self.fact,
|
|
54
|
+
"importance_score": self.importance_score,
|
|
55
|
+
"similarity": score,
|
|
56
|
+
"score": score,
|
|
57
|
+
"created_at": self.created_at,
|
|
58
|
+
"expires_at": self.expires_at,
|
|
59
|
+
"source": self.source,
|
|
60
|
+
"metadata": self.metadata,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LocalGAMockStore:
|
|
65
|
+
"""Deterministic in-memory state for local app contract tests."""
|
|
66
|
+
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
"""Initialize empty response replay and memory state."""
|
|
69
|
+
self.response_replays: dict[str, tuple[str, dict[str, Any]]] = {}
|
|
70
|
+
self.memory_replays: dict[tuple[str, str], tuple[str, dict[str, Any]]] = {}
|
|
71
|
+
self.memories: dict[str, list[MemoryFact]] = {}
|
|
72
|
+
self.profiles: dict[str, dict[str, Any]] = {}
|
|
73
|
+
self.projects: dict[str, dict[str, Any]] = {}
|
|
74
|
+
self.logs_by_project: dict[str, list[dict[str, Any]]] = {}
|
|
75
|
+
self.project_traces: dict[str, list[dict[str, Any]]] = {}
|
|
76
|
+
self.project_usage: dict[str, dict[str, Any]] = {}
|
|
77
|
+
self.api_keys: dict[str, dict[str, Any]] = {}
|
|
78
|
+
|
|
79
|
+
def list_projects(self, headers: Mapping[str, str] | None = None) -> tuple[int, dict[str, Any]]:
|
|
80
|
+
"""Return mock projects visible to any local management key."""
|
|
81
|
+
|
|
82
|
+
scoped_project_id = self._scoped_project_id(headers)
|
|
83
|
+
if scoped_project_id:
|
|
84
|
+
project = self.projects.get(scoped_project_id)
|
|
85
|
+
return HTTPStatus.OK, {"items": [project] if project else []}
|
|
86
|
+
return HTTPStatus.OK, {"items": list(self.projects.values())}
|
|
87
|
+
|
|
88
|
+
def me(self, headers: Mapping[str, str] | None = None) -> tuple[int, dict[str, Any]]:
|
|
89
|
+
"""Return a stable local admin identity for CLI auth preflight."""
|
|
90
|
+
|
|
91
|
+
scoped_project_id = self._scoped_project_id(headers)
|
|
92
|
+
if scoped_project_id:
|
|
93
|
+
return HTTPStatus.OK, {
|
|
94
|
+
"auth_method": "api_key",
|
|
95
|
+
"project_id": scoped_project_id,
|
|
96
|
+
"project_ids": [scoped_project_id],
|
|
97
|
+
}
|
|
98
|
+
return HTTPStatus.OK, {
|
|
99
|
+
"auth_method": "api_key",
|
|
100
|
+
"project_ids": sorted(self.projects),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def deploy_project(
|
|
104
|
+
self, payload: dict[str, Any], project_id: str | None = None
|
|
105
|
+
) -> tuple[int, dict[str, Any]]:
|
|
106
|
+
"""Create or update a mock project from a deploy payload."""
|
|
107
|
+
|
|
108
|
+
yaml_content = str(payload.get("yaml_content") or "")
|
|
109
|
+
manifest = yaml.safe_load(yaml_content) if yaml_content else {}
|
|
110
|
+
manifest = manifest if isinstance(manifest, dict) else {}
|
|
111
|
+
raw_metadata = manifest.get("metadata")
|
|
112
|
+
metadata = cast(dict[str, Any], raw_metadata) if isinstance(raw_metadata, dict) else {}
|
|
113
|
+
slug = str(metadata.get("name") or project_id or "local-mock-project")
|
|
114
|
+
name = str(metadata.get("display_name") or _display_name(slug))
|
|
115
|
+
resolved_id = project_id or f"proj_mock_{_digest_json({'slug': slug})[:12]}"
|
|
116
|
+
project = {
|
|
117
|
+
"id": resolved_id,
|
|
118
|
+
"name": name,
|
|
119
|
+
"slug": slug,
|
|
120
|
+
"status": "active",
|
|
121
|
+
"channels": _manifest_channels(manifest),
|
|
122
|
+
"enabled_tool_ids": _manifest_tool_ids(manifest),
|
|
123
|
+
"soul_content": str(payload.get("soul_content") or ""),
|
|
124
|
+
"skill_contents": [
|
|
125
|
+
str(content) for content in payload.get("skills", []) if isinstance(content, str)
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
self.projects[resolved_id] = project
|
|
129
|
+
self.project_usage.setdefault(resolved_id, {"agent_turns_count": 0, "total_cost_usd": 0.0})
|
|
130
|
+
return HTTPStatus.OK, project
|
|
131
|
+
|
|
132
|
+
def register_openapi_tools(
|
|
133
|
+
self, project_id: str, payload: dict[str, Any]
|
|
134
|
+
) -> tuple[int, dict[str, Any]]:
|
|
135
|
+
"""Pretend to register curated OpenAPI tools for a project."""
|
|
136
|
+
|
|
137
|
+
project = self.projects.get(project_id)
|
|
138
|
+
if project is None:
|
|
139
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
140
|
+
enabled = list(project.get("enabled_tool_ids") or [])
|
|
141
|
+
generated_count = max(len(enabled), 1)
|
|
142
|
+
return HTTPStatus.OK, {
|
|
143
|
+
"generated_count": generated_count,
|
|
144
|
+
"curated_count": generated_count,
|
|
145
|
+
"enabled_tool_ids": enabled,
|
|
146
|
+
"auto_deployed": bool(payload.get("auto_deploy", True)),
|
|
147
|
+
"mcp_server": {"name": f"{project['slug']}-api"},
|
|
148
|
+
"tools": [{"id": tool_id, "risk_level": "low"} for tool_id in enabled],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def list_tools(self) -> tuple[int, dict[str, Any]]:
|
|
152
|
+
"""Return a stable built-in plus generated tool registry shape."""
|
|
153
|
+
|
|
154
|
+
tool_ids = {"web_search", "calendar_read"}
|
|
155
|
+
for project in self.projects.values():
|
|
156
|
+
tool_ids.update(str(tool_id) for tool_id in project.get("enabled_tool_ids") or [])
|
|
157
|
+
return HTTPStatus.OK, {
|
|
158
|
+
"items": [
|
|
159
|
+
{"id": tool_id, "risk_level": "low", "requires_approval": False}
|
|
160
|
+
for tool_id in sorted(tool_ids)
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def test_project(self, project_id: str, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
|
|
165
|
+
"""Run a deterministic mock project test and retain logs/trace rows."""
|
|
166
|
+
|
|
167
|
+
project = self.projects.get(project_id)
|
|
168
|
+
if project is None:
|
|
169
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
170
|
+
message = str(payload.get("message") or "")
|
|
171
|
+
output = _mock_output_text(message, "mock-admin-user")
|
|
172
|
+
trace_id = f"trace_mock_{_digest_json({'project_id': project_id, 'message': message})[:16]}"
|
|
173
|
+
self._append_project_log(project_id, "user", message, trace_id)
|
|
174
|
+
self._append_project_log(project_id, "assistant", output, trace_id)
|
|
175
|
+
response_id = f"resp_mock_{trace_id.removeprefix('trace_mock_')}"
|
|
176
|
+
self.project_traces.setdefault(project_id, []).append(
|
|
177
|
+
{"trace_id": trace_id, "response_id": response_id}
|
|
178
|
+
)
|
|
179
|
+
self.project_usage.setdefault(project_id, {"agent_turns_count": 0, "total_cost_usd": 0.0})
|
|
180
|
+
self.project_usage[project_id]["agent_turns_count"] += 1
|
|
181
|
+
return HTTPStatus.OK, {
|
|
182
|
+
"response": output,
|
|
183
|
+
"response_text": output,
|
|
184
|
+
"warnings": [],
|
|
185
|
+
"metadata": {"trace_id": trace_id},
|
|
186
|
+
"error": None,
|
|
187
|
+
"model_used": "mock/balanced",
|
|
188
|
+
"cost_usd": 0.0,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def project_logs(self, project_id: str, limit: int) -> tuple[int, dict[str, Any]]:
|
|
192
|
+
"""Return retained mock project logs."""
|
|
193
|
+
|
|
194
|
+
return HTTPStatus.OK, {"items": self.logs_by_project.get(project_id, [])[-limit:]}
|
|
195
|
+
|
|
196
|
+
def project_usage_detail(self, project_id: str) -> tuple[int, dict[str, Any]]:
|
|
197
|
+
"""Return retained mock usage counters."""
|
|
198
|
+
|
|
199
|
+
totals = self.project_usage.get(project_id, {"agent_turns_count": 0, "total_cost_usd": 0.0})
|
|
200
|
+
return HTTPStatus.OK, {
|
|
201
|
+
"project_id": project_id,
|
|
202
|
+
"start_date": "2026-01-01",
|
|
203
|
+
"end_date": "2026-01-01",
|
|
204
|
+
"totals": totals,
|
|
205
|
+
"days": [{"date": "2026-01-01", **totals}],
|
|
206
|
+
"limits": {},
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
def project_observability(self, project_id: str) -> tuple[int, dict[str, Any]]:
|
|
210
|
+
"""Return retained mock observability rows."""
|
|
211
|
+
|
|
212
|
+
traces = self.project_traces.get(project_id, [])
|
|
213
|
+
return HTTPStatus.OK, {
|
|
214
|
+
"langfuse_enabled": False,
|
|
215
|
+
"langfuse_project_id": None,
|
|
216
|
+
"langfuse_url": None,
|
|
217
|
+
"traces": traces,
|
|
218
|
+
"metrics": {"trace_count": len(traces)},
|
|
219
|
+
"messages_over_time": [],
|
|
220
|
+
"model_distribution": [{"model": "mock/balanced", "count": len(traces)}],
|
|
221
|
+
"tool_usage": [],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def project_channel_status(self, project_id: str) -> tuple[int, dict[str, Any]]:
|
|
225
|
+
"""Return hosted-compatible channel readiness for one mock project."""
|
|
226
|
+
|
|
227
|
+
project = self.projects.get(project_id)
|
|
228
|
+
if project is None:
|
|
229
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
230
|
+
raw_channels = project.get("channels")
|
|
231
|
+
configured: dict[str, Any] = raw_channels if isinstance(raw_channels, dict) else {}
|
|
232
|
+
channels = [
|
|
233
|
+
_channel_health("in_app", "In-app", configured=True),
|
|
234
|
+
_channel_health("whatsapp", "WhatsApp", configured=bool(configured.get("whatsapp"))),
|
|
235
|
+
_channel_health("telegram", "Telegram", configured=bool(configured.get("telegram"))),
|
|
236
|
+
_channel_health("sms", "SMS", configured=bool(configured.get("sms"))),
|
|
237
|
+
]
|
|
238
|
+
return HTTPStatus.OK, {"project_id": project_id, "channels": channels}
|
|
239
|
+
|
|
240
|
+
def project_runtime_policy(self, project_id: str) -> tuple[int, dict[str, Any]]:
|
|
241
|
+
"""Return a hosted-compatible, secret-free runtime policy summary."""
|
|
242
|
+
|
|
243
|
+
project = self.projects.get(project_id)
|
|
244
|
+
if project is None:
|
|
245
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
246
|
+
enabled_tool_ids = [
|
|
247
|
+
str(tool_id) for tool_id in project.get("enabled_tool_ids") or []
|
|
248
|
+
]
|
|
249
|
+
return HTTPStatus.OK, {
|
|
250
|
+
"project_id": project_id,
|
|
251
|
+
"model_routing": {
|
|
252
|
+
"mode": "tiered_complexity",
|
|
253
|
+
"tiers": {
|
|
254
|
+
"simple": "mock/simple",
|
|
255
|
+
"balanced": "mock/balanced",
|
|
256
|
+
"complex": "mock/complex",
|
|
257
|
+
},
|
|
258
|
+
"default_tier": "balanced",
|
|
259
|
+
"auto_routes_by": [
|
|
260
|
+
"prompt complexity",
|
|
261
|
+
"enabled tools",
|
|
262
|
+
"conversation history",
|
|
263
|
+
"reasoning_effort override when supplied",
|
|
264
|
+
],
|
|
265
|
+
"channel_parity": True,
|
|
266
|
+
},
|
|
267
|
+
"tool_discovery": {
|
|
268
|
+
"mode": "auto",
|
|
269
|
+
"direct_schema_tool_limit": 8,
|
|
270
|
+
"max_search_results": 5,
|
|
271
|
+
},
|
|
272
|
+
"hermes_exposure": {
|
|
273
|
+
"uses_dynamic_discovery_by_default": False,
|
|
274
|
+
"turn_path": "shared_hermes",
|
|
275
|
+
},
|
|
276
|
+
"platform_tools": {
|
|
277
|
+
"enabled_tool_ids": enabled_tool_ids,
|
|
278
|
+
"unknown_tool_ids": [],
|
|
279
|
+
},
|
|
280
|
+
"mcp": {"enabled_tool_ids": []},
|
|
281
|
+
"skills": {
|
|
282
|
+
"names": [
|
|
283
|
+
_skill_name_from_content(content)
|
|
284
|
+
for content in project.get("skill_contents", []) or []
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
def project_soul(self, project_id: str) -> tuple[int, dict[str, Any]]:
|
|
290
|
+
"""Return SOUL.md content for one mock project."""
|
|
291
|
+
|
|
292
|
+
project = self.projects.get(project_id)
|
|
293
|
+
if project is None:
|
|
294
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
295
|
+
return HTTPStatus.OK, {
|
|
296
|
+
"project_id": project_id,
|
|
297
|
+
"content": str(project.get("soul_content") or ""),
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
def project_skills(self, project_id: str) -> tuple[int, dict[str, Any]]:
|
|
301
|
+
"""Return SKILL.md summaries for one mock project."""
|
|
302
|
+
|
|
303
|
+
project = self.projects.get(project_id)
|
|
304
|
+
if project is None:
|
|
305
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
306
|
+
return HTTPStatus.OK, {
|
|
307
|
+
"items": [
|
|
308
|
+
_skill_summary(content)
|
|
309
|
+
for content in project.get("skill_contents", []) or []
|
|
310
|
+
if isinstance(content, str)
|
|
311
|
+
]
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
def get_project_skill(self, project_id: str, skill_name: str) -> tuple[int, dict[str, Any]]:
|
|
315
|
+
"""Return one mock SKILL.md file."""
|
|
316
|
+
|
|
317
|
+
project = self.projects.get(project_id)
|
|
318
|
+
if project is None:
|
|
319
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
320
|
+
for content in project.get("skill_contents", []) or []:
|
|
321
|
+
if isinstance(content, str) and _skill_name_from_content(content) == skill_name:
|
|
322
|
+
return HTTPStatus.OK, {
|
|
323
|
+
"name": skill_name,
|
|
324
|
+
"content": content,
|
|
325
|
+
"metadata": _skill_metadata(content),
|
|
326
|
+
}
|
|
327
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Skill not found.")
|
|
328
|
+
|
|
329
|
+
def add_project_skill(
|
|
330
|
+
self, project_id: str, payload: dict[str, Any]
|
|
331
|
+
) -> tuple[int, dict[str, Any]]:
|
|
332
|
+
"""Create or replace one mock SKILL.md file."""
|
|
333
|
+
|
|
334
|
+
project = self.projects.get(project_id)
|
|
335
|
+
if project is None:
|
|
336
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
337
|
+
content = str(payload.get("content") or "")
|
|
338
|
+
if not content.strip():
|
|
339
|
+
return HTTPStatus.BAD_REQUEST, _error("invalid_request", "Skill content is required.")
|
|
340
|
+
skill_name = _skill_name_from_content(content)
|
|
341
|
+
existing = [
|
|
342
|
+
item
|
|
343
|
+
for item in project.get("skill_contents", []) or []
|
|
344
|
+
if isinstance(item, str) and _skill_name_from_content(item) != skill_name
|
|
345
|
+
]
|
|
346
|
+
project["skill_contents"] = [*existing, content]
|
|
347
|
+
return HTTPStatus.OK, {
|
|
348
|
+
"name": skill_name,
|
|
349
|
+
"content": content,
|
|
350
|
+
"metadata": _skill_metadata(content),
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
def delete_project_skill(self, project_id: str, skill_name: str) -> tuple[int, dict[str, Any]]:
|
|
354
|
+
"""Delete one mock SKILL.md file."""
|
|
355
|
+
|
|
356
|
+
project = self.projects.get(project_id)
|
|
357
|
+
if project is None:
|
|
358
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
359
|
+
existing = [
|
|
360
|
+
item
|
|
361
|
+
for item in project.get("skill_contents", []) or []
|
|
362
|
+
if isinstance(item, str) and _skill_name_from_content(item) != skill_name
|
|
363
|
+
]
|
|
364
|
+
if len(existing) == len(project.get("skill_contents", []) or []):
|
|
365
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Skill not found.")
|
|
366
|
+
project["skill_contents"] = existing
|
|
367
|
+
return HTTPStatus.OK, {"status": "deleted", "name": skill_name}
|
|
368
|
+
|
|
369
|
+
def project_tool_call_audit(self, project_id: str) -> tuple[int, dict[str, Any]]:
|
|
370
|
+
"""Return a stable empty audit list for local verification."""
|
|
371
|
+
|
|
372
|
+
if project_id not in self.projects:
|
|
373
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
|
|
374
|
+
return HTTPStatus.OK, {"items": [], "next_cursor": None}
|
|
375
|
+
|
|
376
|
+
def create_api_key(self, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
|
|
377
|
+
"""Create a deterministic local API key row."""
|
|
378
|
+
|
|
379
|
+
key_id = f"key_mock_{_digest_json(payload)[:12]}"
|
|
380
|
+
raw_key = f"gaadmlocal_{_digest_json({'key_id': key_id})[:24]}"
|
|
381
|
+
row = {
|
|
382
|
+
"id": key_id,
|
|
383
|
+
"name": str(payload.get("name") or "Local mock key"),
|
|
384
|
+
"api_key": raw_key,
|
|
385
|
+
"masked_key": f"{raw_key[:12]}...{raw_key[-4:]}",
|
|
386
|
+
"scopes": payload.get("scopes") or ["admin"],
|
|
387
|
+
"project_id": payload.get("project_id"),
|
|
388
|
+
"expires_at": payload.get("expires_at"),
|
|
389
|
+
"created_by": "local_mock",
|
|
390
|
+
"created_at": MOCK_CREATED_AT,
|
|
391
|
+
"last_used_at": None,
|
|
392
|
+
}
|
|
393
|
+
self.api_keys[key_id] = row
|
|
394
|
+
return HTTPStatus.OK, row
|
|
395
|
+
|
|
396
|
+
def list_api_keys(self, headers: Mapping[str, str] | None = None) -> tuple[int, dict[str, Any]]:
|
|
397
|
+
"""List mock API keys without raw secrets."""
|
|
398
|
+
|
|
399
|
+
scoped_project_id = self._scoped_project_id(headers)
|
|
400
|
+
items = []
|
|
401
|
+
for row in self.api_keys.values():
|
|
402
|
+
if scoped_project_id and str(row.get("project_id") or "") != scoped_project_id:
|
|
403
|
+
continue
|
|
404
|
+
item = dict(row)
|
|
405
|
+
item.pop("api_key", None)
|
|
406
|
+
items.append(item)
|
|
407
|
+
return HTTPStatus.OK, {"items": items}
|
|
408
|
+
|
|
409
|
+
def _scoped_project_id(self, headers: Mapping[str, str] | None) -> str | None:
|
|
410
|
+
"""Return the project id for a raw mock project key."""
|
|
411
|
+
|
|
412
|
+
credential = _auth_credential(headers or {})
|
|
413
|
+
if not credential:
|
|
414
|
+
return None
|
|
415
|
+
for row in self.api_keys.values():
|
|
416
|
+
if row.get("api_key") == credential and row.get("project_id"):
|
|
417
|
+
return str(row["project_id"])
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
def update_api_key(self, key_id: str, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
|
|
421
|
+
"""Update mock API key metadata."""
|
|
422
|
+
|
|
423
|
+
row = self.api_keys.get(key_id)
|
|
424
|
+
if row is None:
|
|
425
|
+
return HTTPStatus.NOT_FOUND, _error("not_found", "API key not found.")
|
|
426
|
+
for key in ("name", "scopes", "expires_at"):
|
|
427
|
+
if key in payload:
|
|
428
|
+
row[key] = payload[key]
|
|
429
|
+
item = dict(row)
|
|
430
|
+
item.pop("api_key", None)
|
|
431
|
+
return HTTPStatus.OK, item
|
|
432
|
+
|
|
433
|
+
def revoke_api_key(self, key_id: str) -> tuple[int, dict[str, Any]]:
|
|
434
|
+
"""Revoke a mock API key."""
|
|
435
|
+
|
|
436
|
+
self.api_keys.pop(key_id, None)
|
|
437
|
+
return HTTPStatus.OK, {"status": "revoked", "id": key_id}
|
|
438
|
+
|
|
439
|
+
def response(
|
|
440
|
+
self,
|
|
441
|
+
payload: dict[str, Any],
|
|
442
|
+
headers: Mapping[str, str],
|
|
443
|
+
) -> tuple[int, dict[str, Any]]:
|
|
444
|
+
"""Return a deterministic Responses-shaped object."""
|
|
445
|
+
idempotency_key = _header(headers, "X-Idempotency-Key")
|
|
446
|
+
request_digest = _digest_json(payload)
|
|
447
|
+
if idempotency_key and idempotency_key in self.response_replays:
|
|
448
|
+
stored_digest, stored_response = self.response_replays[idempotency_key]
|
|
449
|
+
if stored_digest != request_digest:
|
|
450
|
+
return HTTPStatus.CONFLICT, _error(
|
|
451
|
+
"idempotency_key_conflict",
|
|
452
|
+
"X-Idempotency-Key was reused with a different mock request body.",
|
|
453
|
+
)
|
|
454
|
+
return HTTPStatus.OK, copy.deepcopy(stored_response)
|
|
455
|
+
|
|
456
|
+
response = self._build_response(payload, headers, idempotency_key)
|
|
457
|
+
project_id = _header(headers, "X-Project-ID")
|
|
458
|
+
if project_id:
|
|
459
|
+
self._record_response_turn(project_id, payload, response)
|
|
460
|
+
if idempotency_key:
|
|
461
|
+
self.response_replays[idempotency_key] = (request_digest, copy.deepcopy(response))
|
|
462
|
+
return HTTPStatus.OK, response
|
|
463
|
+
|
|
464
|
+
def _record_response_turn(
|
|
465
|
+
self, project_id: str, payload: dict[str, Any], response: dict[str, Any]
|
|
466
|
+
) -> None:
|
|
467
|
+
"""Record an app-facing response in project logs and observability."""
|
|
468
|
+
|
|
469
|
+
input_text = _extract_input_text(payload.get("input"))
|
|
470
|
+
output_text = response["output"][0]["content"][0]["text"]
|
|
471
|
+
trace_id = str(response["metadata"]["general_augment_trace_id"])
|
|
472
|
+
self._append_project_log(project_id, "user", input_text, trace_id)
|
|
473
|
+
self._append_project_log(project_id, "assistant", output_text, trace_id)
|
|
474
|
+
self.project_traces.setdefault(project_id, []).append(
|
|
475
|
+
{"trace_id": trace_id, "response_id": response["id"]}
|
|
476
|
+
)
|
|
477
|
+
self.project_usage.setdefault(project_id, {"agent_turns_count": 0, "total_cost_usd": 0.0})
|
|
478
|
+
self.project_usage[project_id]["agent_turns_count"] += 1
|
|
479
|
+
|
|
480
|
+
def _append_project_log(
|
|
481
|
+
self, project_id: str, role: str, content: str, trace_id: str
|
|
482
|
+
) -> None:
|
|
483
|
+
"""Append one mock project log row."""
|
|
484
|
+
|
|
485
|
+
digest = _digest_json({"project_id": project_id, "role": role, "content": content})
|
|
486
|
+
log_id = f"msg_mock_{digest[:12]}"
|
|
487
|
+
self.logs_by_project.setdefault(project_id, []).append(
|
|
488
|
+
{
|
|
489
|
+
"id": log_id,
|
|
490
|
+
"created_at": MOCK_CREATED_AT,
|
|
491
|
+
"session_id": _mock_user_uuid(f"{project_id}:session"),
|
|
492
|
+
"user_id": _mock_user_uuid(f"{project_id}:user"),
|
|
493
|
+
"role": role,
|
|
494
|
+
"content": content,
|
|
495
|
+
"observability_trace_id": trace_id,
|
|
496
|
+
"model_used": "mock/balanced" if role == "assistant" else None,
|
|
497
|
+
"cost_usd": 0.0 if role == "assistant" else None,
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def store_memory(self, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
|
|
502
|
+
"""Store one explicit memory fact in process memory."""
|
|
503
|
+
user_id = _external_user_id(payload)
|
|
504
|
+
fact = str(payload.get("fact") or payload.get("content") or "").strip()
|
|
505
|
+
if not fact:
|
|
506
|
+
return HTTPStatus.BAD_REQUEST, _error("invalid_memory", "fact is required.")
|
|
507
|
+
|
|
508
|
+
idempotency_key = _optional_string(payload.get("idempotency_key"))
|
|
509
|
+
request_digest = _digest_json(payload)
|
|
510
|
+
replay_key = (user_id, idempotency_key or "")
|
|
511
|
+
if idempotency_key and replay_key in self.memory_replays:
|
|
512
|
+
stored_digest, stored_response = self.memory_replays[replay_key]
|
|
513
|
+
if stored_digest != request_digest:
|
|
514
|
+
return HTTPStatus.CONFLICT, _error(
|
|
515
|
+
"memory_idempotency_key_conflict",
|
|
516
|
+
"idempotency_key was reused with a different memory write request.",
|
|
517
|
+
)
|
|
518
|
+
return HTTPStatus.OK, copy.deepcopy(stored_response)
|
|
519
|
+
|
|
520
|
+
memory_id = f"mem_mock_{_digest_json({'user_id': user_id, 'fact': fact})[:16]}"
|
|
521
|
+
metadata = _object_payload(payload.get("metadata"))
|
|
522
|
+
fact_row = MemoryFact(
|
|
523
|
+
memory_id=memory_id,
|
|
524
|
+
user_id=user_id,
|
|
525
|
+
fact=fact,
|
|
526
|
+
fact_type=str(payload.get("fact_type") or "fact"),
|
|
527
|
+
importance_score=_optional_float(payload.get("importance_score")),
|
|
528
|
+
source=_optional_string(payload.get("source")),
|
|
529
|
+
metadata=dict(metadata),
|
|
530
|
+
)
|
|
531
|
+
self.memories.setdefault(user_id, [])
|
|
532
|
+
if not any(existing.memory_id == memory_id for existing in self.memories[user_id]):
|
|
533
|
+
self.memories[user_id].append(fact_row)
|
|
534
|
+
|
|
535
|
+
user_profile = _object_payload(payload.get("user_profile"))
|
|
536
|
+
if user_profile:
|
|
537
|
+
self.profiles.setdefault(user_id, {}).update(user_profile)
|
|
538
|
+
|
|
539
|
+
response = {
|
|
540
|
+
"user_id": user_id,
|
|
541
|
+
"general_augment_user_id": _mock_user_uuid(user_id),
|
|
542
|
+
"memory_id": memory_id,
|
|
543
|
+
"content": fact,
|
|
544
|
+
"fact": fact,
|
|
545
|
+
"fact_type": fact_row.fact_type,
|
|
546
|
+
"importance_score": fact_row.importance_score,
|
|
547
|
+
"source": fact_row.source,
|
|
548
|
+
"metadata": fact_row.metadata,
|
|
549
|
+
"status": "stored",
|
|
550
|
+
}
|
|
551
|
+
if idempotency_key:
|
|
552
|
+
self.memory_replays[replay_key] = (request_digest, copy.deepcopy(response))
|
|
553
|
+
return HTTPStatus.OK, response
|
|
554
|
+
|
|
555
|
+
def search_memory(self, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
|
|
556
|
+
"""Search stored mock memory with deterministic lexical scores."""
|
|
557
|
+
user_id = _external_user_id(payload)
|
|
558
|
+
query = str(payload.get("query") or "")
|
|
559
|
+
limit = _positive_int(payload.get("limit"), default=10)
|
|
560
|
+
min_similarity = _optional_float(payload.get("min_similarity"))
|
|
561
|
+
min_importance = _optional_float(payload.get("min_importance"))
|
|
562
|
+
fact_type = _optional_string(payload.get("fact_type"))
|
|
563
|
+
source = _optional_string(payload.get("source"))
|
|
564
|
+
|
|
565
|
+
ranked: list[tuple[float, MemoryFact]] = []
|
|
566
|
+
for fact in self.memories.get(user_id, []):
|
|
567
|
+
if fact_type and fact.fact_type != fact_type:
|
|
568
|
+
continue
|
|
569
|
+
if source and fact.source != source:
|
|
570
|
+
continue
|
|
571
|
+
if min_importance is not None and (fact.importance_score or 0.0) < min_importance:
|
|
572
|
+
continue
|
|
573
|
+
score = _lexical_score(query, fact.fact)
|
|
574
|
+
if min_similarity is not None and score < min_similarity:
|
|
575
|
+
continue
|
|
576
|
+
ranked.append((score, fact))
|
|
577
|
+
|
|
578
|
+
ranked.sort(key=lambda item: (-item[0], item[1].memory_id))
|
|
579
|
+
facts = [fact.as_result(round(score, 4)) for score, fact in ranked[:limit]]
|
|
580
|
+
return HTTPStatus.OK, {
|
|
581
|
+
"user_id": user_id,
|
|
582
|
+
"general_augment_user_id": _mock_user_uuid(user_id),
|
|
583
|
+
"facts": facts,
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
def memory_profile(self, user_id: str) -> tuple[int, dict[str, Any]]:
|
|
587
|
+
"""Return the mock profile and recent facts for one app user."""
|
|
588
|
+
facts = self.memories.get(user_id, [])
|
|
589
|
+
profile = {
|
|
590
|
+
"external_user_id": user_id,
|
|
591
|
+
"external_provider": "local_mock",
|
|
592
|
+
**self.profiles.get(user_id, {}),
|
|
593
|
+
}
|
|
594
|
+
return HTTPStatus.OK, {
|
|
595
|
+
"user_id": user_id,
|
|
596
|
+
"general_augment_user_id": _mock_user_uuid(user_id),
|
|
597
|
+
"profile": profile,
|
|
598
|
+
"recent_facts": [fact.as_result(None) for fact in facts],
|
|
599
|
+
"total_facts": len(facts),
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
def delete_memory(self, memory_id: str, user_id: str) -> tuple[int, dict[str, Any]]:
|
|
603
|
+
"""Delete one memory for the scoped app user."""
|
|
604
|
+
facts = self.memories.get(user_id, [])
|
|
605
|
+
kept = [fact for fact in facts if fact.memory_id != memory_id]
|
|
606
|
+
deleted_count = len(facts) - len(kept)
|
|
607
|
+
self.memories[user_id] = kept
|
|
608
|
+
return HTTPStatus.OK, {
|
|
609
|
+
"user_id": user_id,
|
|
610
|
+
"general_augment_user_id": _mock_user_uuid(user_id),
|
|
611
|
+
"memory_id": memory_id,
|
|
612
|
+
"deleted_ids": [memory_id] if deleted_count else [],
|
|
613
|
+
"deleted_count": deleted_count,
|
|
614
|
+
"status": "deleted" if deleted_count else "not_found",
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
def purge_user_memory(self, user_id: str) -> tuple[int, dict[str, Any]]:
|
|
618
|
+
"""Delete all memory rows for one app user."""
|
|
619
|
+
deleted_ids = [fact.memory_id for fact in self.memories.get(user_id, [])]
|
|
620
|
+
self.memories[user_id] = []
|
|
621
|
+
return HTTPStatus.OK, {
|
|
622
|
+
"user_id": user_id,
|
|
623
|
+
"general_augment_user_id": _mock_user_uuid(user_id),
|
|
624
|
+
"memory_id": None,
|
|
625
|
+
"deleted_ids": deleted_ids,
|
|
626
|
+
"deleted_count": len(deleted_ids),
|
|
627
|
+
"status": "purged",
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
def _build_response(
|
|
631
|
+
self,
|
|
632
|
+
payload: dict[str, Any],
|
|
633
|
+
headers: Mapping[str, str],
|
|
634
|
+
idempotency_key: str | None,
|
|
635
|
+
) -> dict[str, Any]:
|
|
636
|
+
"""Build one mock Responses object."""
|
|
637
|
+
input_text = _extract_input_text(payload.get("input"))
|
|
638
|
+
output_text = _mock_response_text(
|
|
639
|
+
payload,
|
|
640
|
+
input_text,
|
|
641
|
+
str(payload.get("user") or "mock-user"),
|
|
642
|
+
)
|
|
643
|
+
digest = _digest_json(
|
|
644
|
+
{
|
|
645
|
+
"input": input_text,
|
|
646
|
+
"user": payload.get("user"),
|
|
647
|
+
"model": payload.get("model"),
|
|
648
|
+
"idempotency_key": idempotency_key,
|
|
649
|
+
}
|
|
650
|
+
)
|
|
651
|
+
response_id = f"resp_mock_{digest[:20]}"
|
|
652
|
+
request_id = _header(headers, "X-Request-ID") or f"req_mock_{digest[:16]}"
|
|
653
|
+
trace_id = f"trace_mock_{digest[:16]}"
|
|
654
|
+
model = str(payload.get("model") or "balanced")
|
|
655
|
+
input_tokens = max(1, len(input_text.split()))
|
|
656
|
+
output_tokens = max(1, len(output_text.split()))
|
|
657
|
+
metadata = _object_payload(payload.get("metadata"))
|
|
658
|
+
response_metadata = dict(metadata)
|
|
659
|
+
response_metadata.update(
|
|
660
|
+
{
|
|
661
|
+
"general_augment_response_id": response_id,
|
|
662
|
+
"general_augment_request_id": request_id,
|
|
663
|
+
"general_augment_trace_id": trace_id,
|
|
664
|
+
"general_augment_model": f"mock/{model}",
|
|
665
|
+
"general_augment_input_tokens": input_tokens,
|
|
666
|
+
"general_augment_output_tokens": output_tokens,
|
|
667
|
+
"general_augment_cost_usd": 0.0,
|
|
668
|
+
"general_augment_latency_ms": 0,
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
response_metadata.setdefault("trace_id", trace_id)
|
|
672
|
+
response_metadata.setdefault("request_id", request_id)
|
|
673
|
+
_copy_trace_header(headers, response_metadata, "traceparent")
|
|
674
|
+
_copy_trace_header(headers, response_metadata, "tracestate")
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
"id": response_id,
|
|
678
|
+
"object": "response",
|
|
679
|
+
"created_at": MOCK_CREATED_AT,
|
|
680
|
+
"status": "completed",
|
|
681
|
+
"model": f"mock/{model}",
|
|
682
|
+
"output": [
|
|
683
|
+
{
|
|
684
|
+
"id": f"msg_mock_{digest[:20]}",
|
|
685
|
+
"type": "message",
|
|
686
|
+
"role": "assistant",
|
|
687
|
+
"status": "completed",
|
|
688
|
+
"content": [
|
|
689
|
+
{
|
|
690
|
+
"type": "output_text",
|
|
691
|
+
"text": output_text,
|
|
692
|
+
"annotations": [],
|
|
693
|
+
}
|
|
694
|
+
],
|
|
695
|
+
}
|
|
696
|
+
],
|
|
697
|
+
"usage": {
|
|
698
|
+
"input_tokens": input_tokens,
|
|
699
|
+
"input_tokens_details": {"cached_tokens": 0},
|
|
700
|
+
"output_tokens": output_tokens,
|
|
701
|
+
"output_tokens_details": {"reasoning_tokens": 0},
|
|
702
|
+
"total_tokens": input_tokens + output_tokens,
|
|
703
|
+
},
|
|
704
|
+
"previous_response_id": payload.get("previous_response_id"),
|
|
705
|
+
"metadata": response_metadata,
|
|
706
|
+
"error": None,
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def sse_events_for_response(response: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
|
|
711
|
+
"""Return semantic SSE events for a completed mock response."""
|
|
712
|
+
output_item = response["output"][0]
|
|
713
|
+
output_text = output_item["content"][0]["text"]
|
|
714
|
+
response_stub = {
|
|
715
|
+
"id": response["id"],
|
|
716
|
+
"object": "response",
|
|
717
|
+
"status": "in_progress",
|
|
718
|
+
"model": response["model"],
|
|
719
|
+
"output": [],
|
|
720
|
+
"metadata": response["metadata"],
|
|
721
|
+
}
|
|
722
|
+
return [
|
|
723
|
+
(
|
|
724
|
+
"response.created",
|
|
725
|
+
{"type": "response.created", "sequence_number": 0, "response": response_stub},
|
|
726
|
+
),
|
|
727
|
+
(
|
|
728
|
+
"response.in_progress",
|
|
729
|
+
{"type": "response.in_progress", "sequence_number": 1, "response": response_stub},
|
|
730
|
+
),
|
|
731
|
+
(
|
|
732
|
+
"response.output_item.added",
|
|
733
|
+
{
|
|
734
|
+
"type": "response.output_item.added",
|
|
735
|
+
"sequence_number": 2,
|
|
736
|
+
"output_index": 0,
|
|
737
|
+
"item": output_item,
|
|
738
|
+
},
|
|
739
|
+
),
|
|
740
|
+
(
|
|
741
|
+
"response.output_text.delta",
|
|
742
|
+
{
|
|
743
|
+
"type": "response.output_text.delta",
|
|
744
|
+
"sequence_number": 3,
|
|
745
|
+
"item_id": output_item["id"],
|
|
746
|
+
"output_index": 0,
|
|
747
|
+
"content_index": 0,
|
|
748
|
+
"delta": output_text,
|
|
749
|
+
},
|
|
750
|
+
),
|
|
751
|
+
(
|
|
752
|
+
"response.output_text.done",
|
|
753
|
+
{
|
|
754
|
+
"type": "response.output_text.done",
|
|
755
|
+
"sequence_number": 4,
|
|
756
|
+
"item_id": output_item["id"],
|
|
757
|
+
"output_index": 0,
|
|
758
|
+
"content_index": 0,
|
|
759
|
+
"text": output_text,
|
|
760
|
+
},
|
|
761
|
+
),
|
|
762
|
+
(
|
|
763
|
+
"response.completed",
|
|
764
|
+
{
|
|
765
|
+
"type": "response.completed",
|
|
766
|
+
"sequence_number": 5,
|
|
767
|
+
"response": response,
|
|
768
|
+
},
|
|
769
|
+
),
|
|
770
|
+
]
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
class LocalGAMockHandler(BaseHTTPRequestHandler):
|
|
774
|
+
"""HTTP handler for the local test mock."""
|
|
775
|
+
|
|
776
|
+
store = LocalGAMockStore()
|
|
777
|
+
quiet = False
|
|
778
|
+
server_version = "GeneralAugmentLocalMock/1.0"
|
|
779
|
+
|
|
780
|
+
def do_GET(self) -> None:
|
|
781
|
+
"""Handle health and memory profile requests."""
|
|
782
|
+
parsed = urlparse(self.path)
|
|
783
|
+
if parsed.path in HEALTH_PATHS:
|
|
784
|
+
self._send_json(
|
|
785
|
+
HTTPStatus.OK,
|
|
786
|
+
{
|
|
787
|
+
"status": "ok",
|
|
788
|
+
"db": "connected (local mock)",
|
|
789
|
+
"redis": "connected (local mock)",
|
|
790
|
+
},
|
|
791
|
+
)
|
|
792
|
+
return
|
|
793
|
+
if parsed.path == "/api/v1/admin/projects":
|
|
794
|
+
status, payload = self.store.list_projects(_headers_dict(self.headers))
|
|
795
|
+
self._send_json(status, payload)
|
|
796
|
+
return
|
|
797
|
+
if parsed.path == "/api/v1/admin/me":
|
|
798
|
+
status, payload = self.store.me(_headers_dict(self.headers))
|
|
799
|
+
self._send_json(status, payload)
|
|
800
|
+
return
|
|
801
|
+
if parsed.path == "/api/v1/admin/tools":
|
|
802
|
+
status, payload = self.store.list_tools()
|
|
803
|
+
self._send_json(status, payload)
|
|
804
|
+
return
|
|
805
|
+
if parsed.path == "/api/v1/admin/keys":
|
|
806
|
+
status, payload = self.store.list_api_keys(_headers_dict(self.headers))
|
|
807
|
+
self._send_json(status, payload)
|
|
808
|
+
return
|
|
809
|
+
admin_match = _admin_project_route(parsed.path)
|
|
810
|
+
if admin_match:
|
|
811
|
+
project_id, suffix = admin_match
|
|
812
|
+
query = parse_qs(parsed.query)
|
|
813
|
+
if suffix == "logs":
|
|
814
|
+
limit = _positive_int(query.get("limit", ["50"])[0], default=50)
|
|
815
|
+
status, payload = self.store.project_logs(project_id, limit)
|
|
816
|
+
self._send_json(status, payload)
|
|
817
|
+
return
|
|
818
|
+
if suffix == "usage":
|
|
819
|
+
status, payload = self.store.project_usage_detail(project_id)
|
|
820
|
+
self._send_json(status, payload)
|
|
821
|
+
return
|
|
822
|
+
if suffix == "observability":
|
|
823
|
+
status, payload = self.store.project_observability(project_id)
|
|
824
|
+
self._send_json(status, payload)
|
|
825
|
+
return
|
|
826
|
+
if suffix == "channels/status":
|
|
827
|
+
status, payload = self.store.project_channel_status(project_id)
|
|
828
|
+
self._send_json(status, payload)
|
|
829
|
+
return
|
|
830
|
+
if suffix == "runtime-policy":
|
|
831
|
+
status, payload = self.store.project_runtime_policy(project_id)
|
|
832
|
+
self._send_json(status, payload)
|
|
833
|
+
return
|
|
834
|
+
if suffix == "soul":
|
|
835
|
+
status, payload = self.store.project_soul(project_id)
|
|
836
|
+
self._send_json(status, payload)
|
|
837
|
+
return
|
|
838
|
+
if suffix == "skills":
|
|
839
|
+
status, payload = self.store.project_skills(project_id)
|
|
840
|
+
self._send_json(status, payload)
|
|
841
|
+
return
|
|
842
|
+
if suffix.startswith("skills/"):
|
|
843
|
+
skill_name = unquote(suffix.removeprefix("skills/"))
|
|
844
|
+
status, payload = self.store.get_project_skill(project_id, skill_name)
|
|
845
|
+
self._send_json(status, payload)
|
|
846
|
+
return
|
|
847
|
+
if suffix == "audit/tool-calls":
|
|
848
|
+
status, payload = self.store.project_tool_call_audit(project_id)
|
|
849
|
+
self._send_json(status, payload)
|
|
850
|
+
return
|
|
851
|
+
if parsed.path.startswith("/api/v1/agent/memory/profile/"):
|
|
852
|
+
user_id = _path_suffix(parsed.path, "/api/v1/agent/memory/profile/")
|
|
853
|
+
status, payload = self.store.memory_profile(user_id)
|
|
854
|
+
self._send_json(status, payload)
|
|
855
|
+
return
|
|
856
|
+
self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
|
|
857
|
+
|
|
858
|
+
def do_POST(self) -> None:
|
|
859
|
+
"""Handle Responses and memory write/search requests."""
|
|
860
|
+
parsed = urlparse(self.path)
|
|
861
|
+
payload = self._read_json()
|
|
862
|
+
if payload is None:
|
|
863
|
+
return
|
|
864
|
+
if parsed.path == "/v1/responses":
|
|
865
|
+
status, response = self.store.response(payload, _headers_dict(self.headers))
|
|
866
|
+
if status == HTTPStatus.OK and payload.get("stream") is True:
|
|
867
|
+
self._send_sse(response)
|
|
868
|
+
return
|
|
869
|
+
self._send_json(status, response)
|
|
870
|
+
return
|
|
871
|
+
if parsed.path == "/api/v1/admin/projects/from-config":
|
|
872
|
+
status, response = self.store.deploy_project(payload)
|
|
873
|
+
self._send_json(status, response)
|
|
874
|
+
return
|
|
875
|
+
if parsed.path == "/api/v1/admin/keys":
|
|
876
|
+
status, response = self.store.create_api_key(payload)
|
|
877
|
+
self._send_json(status, response)
|
|
878
|
+
return
|
|
879
|
+
admin_match = _admin_project_route(parsed.path)
|
|
880
|
+
if admin_match:
|
|
881
|
+
project_id, suffix = admin_match
|
|
882
|
+
if suffix == "tools/from-openapi":
|
|
883
|
+
status, response = self.store.register_openapi_tools(project_id, payload)
|
|
884
|
+
self._send_json(status, response)
|
|
885
|
+
return
|
|
886
|
+
if suffix == "test":
|
|
887
|
+
status, response = self.store.test_project(project_id, payload)
|
|
888
|
+
self._send_json(status, response)
|
|
889
|
+
return
|
|
890
|
+
if suffix == "skills":
|
|
891
|
+
status, response = self.store.add_project_skill(project_id, payload)
|
|
892
|
+
self._send_json(status, response)
|
|
893
|
+
return
|
|
894
|
+
if parsed.path == "/api/v1/agent/memory/store":
|
|
895
|
+
status, response = self.store.store_memory(payload)
|
|
896
|
+
self._send_json(status, response)
|
|
897
|
+
return
|
|
898
|
+
if parsed.path == "/api/v1/agent/memory/search":
|
|
899
|
+
status, response = self.store.search_memory(payload)
|
|
900
|
+
self._send_json(status, response)
|
|
901
|
+
return
|
|
902
|
+
self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
|
|
903
|
+
|
|
904
|
+
def do_DELETE(self) -> None:
|
|
905
|
+
"""Handle memory delete and purge requests."""
|
|
906
|
+
parsed = urlparse(self.path)
|
|
907
|
+
if parsed.path.startswith("/api/v1/agent/memory/user/"):
|
|
908
|
+
user_id = _path_suffix(parsed.path, "/api/v1/agent/memory/user/")
|
|
909
|
+
status, payload = self.store.purge_user_memory(user_id)
|
|
910
|
+
self._send_json(status, payload)
|
|
911
|
+
return
|
|
912
|
+
if parsed.path.startswith("/api/v1/agent/memory/"):
|
|
913
|
+
memory_id = _path_suffix(parsed.path, "/api/v1/agent/memory/")
|
|
914
|
+
query = parse_qs(parsed.query)
|
|
915
|
+
user_id = query.get("user_id", [""])[0]
|
|
916
|
+
status, payload = self.store.delete_memory(memory_id, user_id)
|
|
917
|
+
self._send_json(status, payload)
|
|
918
|
+
return
|
|
919
|
+
if parsed.path.startswith("/api/v1/admin/keys/"):
|
|
920
|
+
key_id = _path_suffix(parsed.path, "/api/v1/admin/keys/")
|
|
921
|
+
status, payload = self.store.revoke_api_key(key_id)
|
|
922
|
+
self._send_json(status, payload)
|
|
923
|
+
return
|
|
924
|
+
admin_match = _admin_project_route(parsed.path)
|
|
925
|
+
if admin_match:
|
|
926
|
+
project_id, suffix = admin_match
|
|
927
|
+
if suffix.startswith("skills/"):
|
|
928
|
+
skill_name = unquote(suffix.removeprefix("skills/"))
|
|
929
|
+
status, payload = self.store.delete_project_skill(project_id, skill_name)
|
|
930
|
+
self._send_json(status, payload)
|
|
931
|
+
return
|
|
932
|
+
self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
|
|
933
|
+
|
|
934
|
+
def do_PUT(self) -> None:
|
|
935
|
+
"""Handle project config updates."""
|
|
936
|
+
|
|
937
|
+
parsed = urlparse(self.path)
|
|
938
|
+
payload = self._read_json()
|
|
939
|
+
if payload is None:
|
|
940
|
+
return
|
|
941
|
+
admin_match = _admin_project_route(parsed.path)
|
|
942
|
+
if admin_match:
|
|
943
|
+
project_id, suffix = admin_match
|
|
944
|
+
if suffix == "config":
|
|
945
|
+
status, response = self.store.deploy_project(payload, project_id=project_id)
|
|
946
|
+
self._send_json(status, response)
|
|
947
|
+
return
|
|
948
|
+
if suffix.startswith("skills/"):
|
|
949
|
+
status, response = self.store.add_project_skill(project_id, payload)
|
|
950
|
+
self._send_json(status, response)
|
|
951
|
+
return
|
|
952
|
+
self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
|
|
953
|
+
|
|
954
|
+
def do_PATCH(self) -> None:
|
|
955
|
+
"""Handle API key metadata updates."""
|
|
956
|
+
|
|
957
|
+
parsed = urlparse(self.path)
|
|
958
|
+
payload = self._read_json()
|
|
959
|
+
if payload is None:
|
|
960
|
+
return
|
|
961
|
+
if parsed.path.startswith("/api/v1/admin/keys/"):
|
|
962
|
+
key_id = _path_suffix(parsed.path, "/api/v1/admin/keys/")
|
|
963
|
+
status, response = self.store.update_api_key(key_id, payload)
|
|
964
|
+
self._send_json(status, response)
|
|
965
|
+
return
|
|
966
|
+
self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
|
|
967
|
+
|
|
968
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
969
|
+
"""Keep test output quiet unless the server is run verbosely."""
|
|
970
|
+
if not self.quiet:
|
|
971
|
+
super().log_message(format, *args)
|
|
972
|
+
|
|
973
|
+
def _read_json(self) -> dict[str, Any] | None:
|
|
974
|
+
"""Read a JSON object request body."""
|
|
975
|
+
length = int(self.headers.get("Content-Length") or "0")
|
|
976
|
+
raw = self.rfile.read(length) if length else b"{}"
|
|
977
|
+
try:
|
|
978
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
979
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
980
|
+
self._send_json(HTTPStatus.BAD_REQUEST, _error("invalid_json", "Body must be JSON."))
|
|
981
|
+
return None
|
|
982
|
+
if not isinstance(payload, dict):
|
|
983
|
+
self._send_json(
|
|
984
|
+
HTTPStatus.BAD_REQUEST,
|
|
985
|
+
_error("invalid_json", "Body must be an object."),
|
|
986
|
+
)
|
|
987
|
+
return None
|
|
988
|
+
return payload
|
|
989
|
+
|
|
990
|
+
def _send_json(self, status: int, payload: dict[str, Any]) -> None:
|
|
991
|
+
"""Send a JSON response."""
|
|
992
|
+
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
993
|
+
self.send_response(int(status))
|
|
994
|
+
self.send_header("Content-Type", "application/json")
|
|
995
|
+
self.send_header("Content-Length", str(len(body)))
|
|
996
|
+
self.end_headers()
|
|
997
|
+
self.wfile.write(body)
|
|
998
|
+
|
|
999
|
+
def _send_sse(self, response: dict[str, Any]) -> None:
|
|
1000
|
+
"""Send a semantic SSE response."""
|
|
1001
|
+
self.send_response(HTTPStatus.OK)
|
|
1002
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
1003
|
+
self.send_header("Cache-Control", "no-cache")
|
|
1004
|
+
self.end_headers()
|
|
1005
|
+
for event, payload in sse_events_for_response(response):
|
|
1006
|
+
data = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
1007
|
+
self.wfile.write(f"event: {event}\ndata: {data}\n\n".encode())
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def make_handler(store: LocalGAMockStore, *, quiet: bool) -> type[LocalGAMockHandler]:
|
|
1011
|
+
"""Return a handler class bound to one mock store."""
|
|
1012
|
+
|
|
1013
|
+
class BoundLocalGAMockHandler(LocalGAMockHandler):
|
|
1014
|
+
"""Handler with injected process-local state."""
|
|
1015
|
+
|
|
1016
|
+
BoundLocalGAMockHandler.store = store
|
|
1017
|
+
BoundLocalGAMockHandler.quiet = quiet
|
|
1018
|
+
return BoundLocalGAMockHandler
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
1022
|
+
"""Parse local mock CLI arguments."""
|
|
1023
|
+
parser = argparse.ArgumentParser(description="Run a local General Augment mock server.")
|
|
1024
|
+
parser.add_argument("--host", default=DEFAULT_HOST)
|
|
1025
|
+
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
|
1026
|
+
parser.add_argument("--quiet", action="store_true")
|
|
1027
|
+
return parser.parse_args(argv)
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def run_server(host: str, port: int, *, quiet: bool = False) -> None:
|
|
1031
|
+
"""Run the mock HTTP server until interrupted."""
|
|
1032
|
+
store = LocalGAMockStore()
|
|
1033
|
+
handler = make_handler(store, quiet=quiet)
|
|
1034
|
+
server = ThreadingHTTPServer((host, port), handler)
|
|
1035
|
+
print(f"General Augment local mock listening at http://{host}:{port}", flush=True)
|
|
1036
|
+
try:
|
|
1037
|
+
server.serve_forever()
|
|
1038
|
+
finally:
|
|
1039
|
+
server.server_close()
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1043
|
+
"""CLI entrypoint."""
|
|
1044
|
+
args = parse_args(argv)
|
|
1045
|
+
try:
|
|
1046
|
+
run_server(args.host, args.port, quiet=args.quiet)
|
|
1047
|
+
except KeyboardInterrupt:
|
|
1048
|
+
print("\nStopped General Augment local mock.", file=sys.stderr)
|
|
1049
|
+
return 0
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _header(headers: Mapping[str, str], name: str) -> str | None:
|
|
1053
|
+
"""Return a case-insensitive header value from mapping-like headers."""
|
|
1054
|
+
for key, value in headers.items():
|
|
1055
|
+
if key.lower() == name.lower():
|
|
1056
|
+
return str(value)
|
|
1057
|
+
return None
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def _headers_dict(headers: Any) -> dict[str, str]:
|
|
1061
|
+
"""Return a plain string mapping for request headers."""
|
|
1062
|
+
return {str(key): str(value) for key, value in headers.items()}
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def _auth_credential(headers: Mapping[str, str]) -> str | None:
|
|
1066
|
+
"""Return either admin or bearer credential from request headers."""
|
|
1067
|
+
|
|
1068
|
+
admin_key = _header(headers, "X-Admin-Key")
|
|
1069
|
+
if admin_key:
|
|
1070
|
+
return admin_key
|
|
1071
|
+
authorization = _header(headers, "Authorization")
|
|
1072
|
+
if authorization and authorization.lower().startswith("bearer "):
|
|
1073
|
+
return authorization.split(None, 1)[1]
|
|
1074
|
+
return None
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _path_suffix(path: str, prefix: str) -> str:
|
|
1078
|
+
"""Return a URL-decoded path suffix after a known route prefix."""
|
|
1079
|
+
return unquote(path.removeprefix(prefix))
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _admin_project_route(path: str) -> tuple[str, str] | None:
|
|
1083
|
+
"""Parse /api/v1/admin/projects/{project_id}/{suffix} routes."""
|
|
1084
|
+
|
|
1085
|
+
prefix = "/api/v1/admin/projects/"
|
|
1086
|
+
if not path.startswith(prefix):
|
|
1087
|
+
return None
|
|
1088
|
+
tail = path.removeprefix(prefix)
|
|
1089
|
+
if "/" not in tail:
|
|
1090
|
+
return unquote(tail), ""
|
|
1091
|
+
raw_project_id, suffix = tail.split("/", 1)
|
|
1092
|
+
return unquote(raw_project_id), suffix
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def _manifest_tool_ids(manifest: dict[str, Any]) -> list[str]:
|
|
1096
|
+
"""Extract enabled generated tool ids from a manifest."""
|
|
1097
|
+
|
|
1098
|
+
tools = manifest.get("tools") if isinstance(manifest.get("tools"), dict) else {}
|
|
1099
|
+
mcp_servers = tools.get("mcp") if isinstance(tools, dict) else []
|
|
1100
|
+
tool_ids: list[str] = []
|
|
1101
|
+
if not isinstance(mcp_servers, list):
|
|
1102
|
+
return tool_ids
|
|
1103
|
+
for server in mcp_servers:
|
|
1104
|
+
if not isinstance(server, dict):
|
|
1105
|
+
continue
|
|
1106
|
+
include = _manifest_server_include(server)
|
|
1107
|
+
if isinstance(include, list):
|
|
1108
|
+
tool_ids.extend(str(tool_id) for tool_id in include)
|
|
1109
|
+
return sorted(set(tool_ids))
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _manifest_server_include(server: dict[str, Any]) -> list[Any] | None:
|
|
1113
|
+
"""Return tool includes from legacy or current generated MCP server shape."""
|
|
1114
|
+
|
|
1115
|
+
include = server.get("include")
|
|
1116
|
+
nested_tools = server.get("tools")
|
|
1117
|
+
if include is None and isinstance(nested_tools, dict):
|
|
1118
|
+
include = nested_tools.get("include")
|
|
1119
|
+
return include if isinstance(include, list) else None
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _manifest_channels(manifest: dict[str, Any]) -> dict[str, Any]:
|
|
1123
|
+
"""Extract configured channel blocks from a generated agent manifest."""
|
|
1124
|
+
|
|
1125
|
+
channels = manifest.get("channels")
|
|
1126
|
+
if not isinstance(channels, dict):
|
|
1127
|
+
return {}
|
|
1128
|
+
return {str(name): value for name, value in channels.items() if isinstance(value, dict)}
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _skill_metadata(content: str) -> dict[str, Any]:
|
|
1132
|
+
"""Return frontmatter metadata from a local mock SKILL.md file."""
|
|
1133
|
+
|
|
1134
|
+
if not content.startswith("---"):
|
|
1135
|
+
return {}
|
|
1136
|
+
parts = content.split("---", 2)
|
|
1137
|
+
if len(parts) < 3:
|
|
1138
|
+
return {}
|
|
1139
|
+
loaded = yaml.safe_load(parts[1]) or {}
|
|
1140
|
+
return loaded if isinstance(loaded, dict) else {}
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _skill_name_from_content(content: str) -> str:
|
|
1144
|
+
"""Return the display name for a local mock SKILL.md file."""
|
|
1145
|
+
|
|
1146
|
+
metadata = _skill_metadata(content)
|
|
1147
|
+
name = metadata.get("name")
|
|
1148
|
+
if isinstance(name, str) and name.strip():
|
|
1149
|
+
return name.strip()
|
|
1150
|
+
for line in content.splitlines():
|
|
1151
|
+
stripped = line.strip()
|
|
1152
|
+
if stripped.startswith("#"):
|
|
1153
|
+
return stripped.lstrip("#").strip() or "Untitled Skill"
|
|
1154
|
+
return "Untitled Skill"
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def _skill_summary(content: str) -> dict[str, Any]:
|
|
1158
|
+
"""Return a hosted-like skill summary."""
|
|
1159
|
+
|
|
1160
|
+
metadata = _skill_metadata(content)
|
|
1161
|
+
return {
|
|
1162
|
+
"name": _skill_name_from_content(content),
|
|
1163
|
+
"description": str(metadata.get("description") or ""),
|
|
1164
|
+
"version": str(metadata.get("version") or "1.0"),
|
|
1165
|
+
"tags": [str(tag) for tag in metadata.get("tags", [])]
|
|
1166
|
+
if isinstance(metadata.get("tags"), list)
|
|
1167
|
+
else [],
|
|
1168
|
+
"tools": [str(tool) for tool in metadata.get("tools", [])]
|
|
1169
|
+
if isinstance(metadata.get("tools"), list)
|
|
1170
|
+
else [],
|
|
1171
|
+
"path": f"skills/{_slug(_skill_name_from_content(content))}/SKILL.md",
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def _channel_health(channel: str, label: str, *, configured: bool) -> dict[str, Any]:
|
|
1176
|
+
"""Return one hosted-like local channel health row."""
|
|
1177
|
+
|
|
1178
|
+
return {
|
|
1179
|
+
"channel": channel,
|
|
1180
|
+
"label": label,
|
|
1181
|
+
"status": "configured" if configured else "open",
|
|
1182
|
+
"sender": f"local-mock:{channel}" if configured else None,
|
|
1183
|
+
"provider_status": "local_mock",
|
|
1184
|
+
"delivery": "available" if configured else "not_configured",
|
|
1185
|
+
"last_message_at": None,
|
|
1186
|
+
"message_count_24h": 0,
|
|
1187
|
+
"details": {},
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def _object_payload(value: Any) -> dict[str, Any]:
|
|
1192
|
+
"""Return a string-keyed object payload or an empty mapping."""
|
|
1193
|
+
if not isinstance(value, dict):
|
|
1194
|
+
return {}
|
|
1195
|
+
return {str(key): item for key, item in value.items()}
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def _copy_trace_header(
|
|
1199
|
+
headers: Mapping[str, str],
|
|
1200
|
+
metadata: dict[str, Any],
|
|
1201
|
+
header_name: str,
|
|
1202
|
+
) -> None:
|
|
1203
|
+
"""Copy bounded trace context into mock metadata."""
|
|
1204
|
+
value = _header(headers, header_name)
|
|
1205
|
+
if not value or len(value) > 512:
|
|
1206
|
+
return
|
|
1207
|
+
metadata[f"general_augment_{header_name}"] = value
|
|
1208
|
+
metadata.setdefault(header_name, value)
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def _digest_json(value: Any) -> str:
|
|
1212
|
+
"""Return a stable digest for JSON-compatible values."""
|
|
1213
|
+
encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
|
|
1214
|
+
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def _error(code: str, message: str) -> dict[str, Any]:
|
|
1218
|
+
"""Return a small General Augment-like error body."""
|
|
1219
|
+
return {
|
|
1220
|
+
"code": code,
|
|
1221
|
+
"reason": code,
|
|
1222
|
+
"message": message,
|
|
1223
|
+
"detail": {"code": code, "reason": code, "message": message},
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
def _extract_input_text(value: Any) -> str:
|
|
1228
|
+
"""Extract readable text from common Responses input shapes."""
|
|
1229
|
+
if value is None:
|
|
1230
|
+
return ""
|
|
1231
|
+
if isinstance(value, str):
|
|
1232
|
+
return value
|
|
1233
|
+
if isinstance(value, list):
|
|
1234
|
+
return " ".join(part for item in value if (part := _extract_input_text(item)))
|
|
1235
|
+
if isinstance(value, dict):
|
|
1236
|
+
if "content" in value:
|
|
1237
|
+
return _extract_input_text(value["content"])
|
|
1238
|
+
if "text" in value:
|
|
1239
|
+
return str(value["text"])
|
|
1240
|
+
if "input_text" in value:
|
|
1241
|
+
return str(value["input_text"])
|
|
1242
|
+
return json.dumps(value, sort_keys=True, default=str)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def _mock_output_text(input_text: str, user_id: str) -> str:
|
|
1246
|
+
"""Return deterministic mock assistant text."""
|
|
1247
|
+
exact = EXACT_REPLY_RE.search(input_text)
|
|
1248
|
+
if exact:
|
|
1249
|
+
return exact.group(1).strip().strip('"`')
|
|
1250
|
+
clipped = " ".join(input_text.split())[:160] or "empty input"
|
|
1251
|
+
return f"Mock General Augment response for {user_id}: {clipped}"
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def _mock_response_text(payload: dict[str, Any], input_text: str, user_id: str) -> str:
|
|
1255
|
+
"""Return deterministic text or schema-shaped JSON for a mock response."""
|
|
1256
|
+
text_format = _object_payload(_object_payload(payload.get("text")).get("format"))
|
|
1257
|
+
if text_format.get("type") == "json_schema":
|
|
1258
|
+
schema = _object_payload(text_format.get("schema"))
|
|
1259
|
+
value = _mock_value_for_schema(schema, input_text)
|
|
1260
|
+
return json.dumps(value, sort_keys=True)
|
|
1261
|
+
if text_format.get("type") == "json_object":
|
|
1262
|
+
return json.dumps({"ok": True, "label": _structured_label(input_text)}, sort_keys=True)
|
|
1263
|
+
return _mock_output_text(input_text, user_id)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def _mock_value_for_schema(schema: dict[str, Any], input_text: str) -> Any:
|
|
1267
|
+
"""Create a small valid-ish JSON value for common smoke-test schemas."""
|
|
1268
|
+
if "const" in schema:
|
|
1269
|
+
return schema["const"]
|
|
1270
|
+
enum = schema.get("enum")
|
|
1271
|
+
if isinstance(enum, list) and enum:
|
|
1272
|
+
return enum[0]
|
|
1273
|
+
|
|
1274
|
+
schema_type = schema.get("type")
|
|
1275
|
+
if isinstance(schema_type, list):
|
|
1276
|
+
schema_type = next((item for item in schema_type if item != "null"), schema_type[0])
|
|
1277
|
+
|
|
1278
|
+
if schema_type == "object" or (
|
|
1279
|
+
schema_type is None and isinstance(schema.get("properties"), dict)
|
|
1280
|
+
):
|
|
1281
|
+
properties = _object_payload(schema.get("properties"))
|
|
1282
|
+
required = schema.get("required")
|
|
1283
|
+
if isinstance(required, list) and required:
|
|
1284
|
+
names = [str(name) for name in required if str(name) in properties]
|
|
1285
|
+
else:
|
|
1286
|
+
names = list(properties)
|
|
1287
|
+
return {
|
|
1288
|
+
name: _mock_named_value(name, _object_payload(properties.get(name)), input_text)
|
|
1289
|
+
for name in names
|
|
1290
|
+
}
|
|
1291
|
+
if schema_type == "array":
|
|
1292
|
+
return [_mock_value_for_schema(_object_payload(schema.get("items")), input_text)]
|
|
1293
|
+
if schema_type == "boolean":
|
|
1294
|
+
return True
|
|
1295
|
+
if schema_type == "integer":
|
|
1296
|
+
return 1
|
|
1297
|
+
if schema_type == "number":
|
|
1298
|
+
return 1.0
|
|
1299
|
+
if schema_type == "null":
|
|
1300
|
+
return None
|
|
1301
|
+
return _structured_label(input_text)
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def _mock_named_value(name: str, schema: dict[str, Any], input_text: str) -> Any:
|
|
1305
|
+
"""Return a deterministic value with nicer defaults for common smoke fields."""
|
|
1306
|
+
normalized = name.lower().replace("-", "_")
|
|
1307
|
+
if normalized in {"ok", "success", "passed", "valid"} and "type" not in schema:
|
|
1308
|
+
return True
|
|
1309
|
+
if normalized in {"label", "status", "message", "summary"} and "type" not in schema:
|
|
1310
|
+
return _structured_label(input_text)
|
|
1311
|
+
return _mock_value_for_schema(schema, input_text)
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def _structured_label(input_text: str) -> str:
|
|
1315
|
+
"""Return a stable label for structured mock responses."""
|
|
1316
|
+
exact = EXACT_REPLY_RE.search(input_text)
|
|
1317
|
+
if exact:
|
|
1318
|
+
return exact.group(1).strip().strip('"`')
|
|
1319
|
+
if "genaug-structured-ok" in input_text:
|
|
1320
|
+
return "genaug-structured-ok"
|
|
1321
|
+
if "genaug-smoke-ok" in input_text:
|
|
1322
|
+
return "genaug-smoke-ok"
|
|
1323
|
+
return "genaug-smoke-ok"
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
def _display_name(value: str) -> str:
|
|
1327
|
+
"""Create a readable display name from a slug."""
|
|
1328
|
+
|
|
1329
|
+
return value.replace("-", " ").replace("_", " ").title()
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def _slug(value: str) -> str:
|
|
1333
|
+
"""Create a stable local mock slug."""
|
|
1334
|
+
|
|
1335
|
+
slug = re.sub(r"[^a-zA-Z0-9_-]+", "-", value.strip().lower()).strip("-")
|
|
1336
|
+
return slug or "skill"
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
def _external_user_id(payload: dict[str, Any]) -> str:
|
|
1340
|
+
"""Resolve app user fields accepted by the mock memory endpoints."""
|
|
1341
|
+
return str(payload.get("user_id") or payload.get("user") or "mock-user")
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def _mock_user_uuid(user_id: str) -> str:
|
|
1345
|
+
"""Return a deterministic UUID-shaped General Augment user id."""
|
|
1346
|
+
return str(uuid5(NAMESPACE_URL, f"general-augment-local-mock:{user_id}"))
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _optional_string(value: Any) -> str | None:
|
|
1350
|
+
"""Return a non-empty string or None."""
|
|
1351
|
+
if value is None:
|
|
1352
|
+
return None
|
|
1353
|
+
text = str(value)
|
|
1354
|
+
return text or None
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def _optional_float(value: Any) -> float | None:
|
|
1358
|
+
"""Return a float if the value is present and numeric."""
|
|
1359
|
+
if value is None:
|
|
1360
|
+
return None
|
|
1361
|
+
try:
|
|
1362
|
+
return float(value)
|
|
1363
|
+
except (TypeError, ValueError):
|
|
1364
|
+
return None
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def _positive_int(value: Any, *, default: int) -> int:
|
|
1368
|
+
"""Parse a positive integer with a default."""
|
|
1369
|
+
try:
|
|
1370
|
+
parsed = int(value)
|
|
1371
|
+
except (TypeError, ValueError):
|
|
1372
|
+
return default
|
|
1373
|
+
return max(1, parsed)
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def _lexical_score(query: str, fact: str) -> float:
|
|
1377
|
+
"""Return a deterministic lexical similarity score."""
|
|
1378
|
+
normalized_query = query.strip().lower()
|
|
1379
|
+
normalized_fact = fact.lower()
|
|
1380
|
+
if not normalized_query:
|
|
1381
|
+
return 1.0
|
|
1382
|
+
if normalized_query in normalized_fact:
|
|
1383
|
+
return 0.99
|
|
1384
|
+
query_terms = set(normalized_query.split())
|
|
1385
|
+
fact_terms = set(normalized_fact.split())
|
|
1386
|
+
if not query_terms:
|
|
1387
|
+
return 1.0
|
|
1388
|
+
return len(query_terms & fact_terms) / len(query_terms)
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
if __name__ == "__main__":
|
|
1392
|
+
raise SystemExit(main())
|