licos-agent-runtime 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.
- licos_agent_runtime-0.1.0/.gitignore +44 -0
- licos_agent_runtime-0.1.0/PKG-INFO +17 -0
- licos_agent_runtime-0.1.0/pyproject.toml +29 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/__init__.py +18 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/__main__.py +69 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/app.py +187 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/context.py +91 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/errors.py +50 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/graph_loader.py +296 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/logging.py +44 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/openai.py +98 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/parser.py +63 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/payload.py +132 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/run_config.py +28 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/service.py +322 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/stream_protocol.py +373 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/stream_runner.py +241 -0
- licos_agent_runtime-0.1.0/src/licos_agent_runtime/telemetry.py +22 -0
- licos_agent_runtime-0.1.0/tests/test_context.py +34 -0
- licos_agent_runtime-0.1.0/tests/test_graph_loader.py +118 -0
- licos_agent_runtime-0.1.0/tests/test_parser.py +40 -0
- licos_agent_runtime-0.1.0/tests/test_payload.py +35 -0
- licos_agent_runtime-0.1.0/tests/test_service_stream_protocol.py +59 -0
- licos_agent_runtime-0.1.0/tests/test_stream_protocol.py +73 -0
- licos_agent_runtime-0.1.0/tests/test_stream_runner.py +64 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
/target/
|
|
3
|
+
**/*.rs.bk
|
|
4
|
+
Cargo.lock
|
|
5
|
+
|
|
6
|
+
# Node
|
|
7
|
+
node_modules/
|
|
8
|
+
packages/*/dist/
|
|
9
|
+
|
|
10
|
+
# IDE
|
|
11
|
+
.idea/
|
|
12
|
+
.vscode/
|
|
13
|
+
*.swp
|
|
14
|
+
*.swo
|
|
15
|
+
|
|
16
|
+
# OS
|
|
17
|
+
.DS_Store
|
|
18
|
+
Thumbs.db
|
|
19
|
+
|
|
20
|
+
# Environment
|
|
21
|
+
.env
|
|
22
|
+
.env.local
|
|
23
|
+
|
|
24
|
+
# Workspace
|
|
25
|
+
/workspace/
|
|
26
|
+
|
|
27
|
+
# Test runtime data
|
|
28
|
+
/test/licos-data/
|
|
29
|
+
/test/screenshots/
|
|
30
|
+
/test/vue-app/
|
|
31
|
+
|
|
32
|
+
# Build
|
|
33
|
+
*.log
|
|
34
|
+
.licos
|
|
35
|
+
|
|
36
|
+
/tmp
|
|
37
|
+
|
|
38
|
+
/Docs/hermes-agent
|
|
39
|
+
/Docs/OpenCode
|
|
40
|
+
/Docs/平台API
|
|
41
|
+
|
|
42
|
+
*.codex-*
|
|
43
|
+
|
|
44
|
+
project-20260423_130440
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: licos-agent-runtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LICOS Python agent runtime for LangGraph/LangChain projects
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: fastapi<1,>=0.121
|
|
7
|
+
Requires-Dist: langchain-openai<2,>=1.2
|
|
8
|
+
Requires-Dist: langchain<2,>=1.2
|
|
9
|
+
Requires-Dist: langgraph-checkpoint-postgres<4,>=3.0
|
|
10
|
+
Requires-Dist: langgraph-checkpoint<5,>=4
|
|
11
|
+
Requires-Dist: langgraph-prebuilt<2,>=1.0
|
|
12
|
+
Requires-Dist: langgraph-sdk<1,>=0.3
|
|
13
|
+
Requires-Dist: langgraph<2,>=1.1
|
|
14
|
+
Requires-Dist: langsmith<1,>=0.4
|
|
15
|
+
Requires-Dist: licos-platform-sdk>=0.1.0
|
|
16
|
+
Requires-Dist: pydantic<3,>=2.12
|
|
17
|
+
Requires-Dist: uvicorn<1,>=0.38
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "licos-agent-runtime"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LICOS Python agent runtime for LangGraph/LangChain projects"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi>=0.121,<1",
|
|
12
|
+
"uvicorn>=0.38,<1",
|
|
13
|
+
"pydantic>=2.12,<3",
|
|
14
|
+
"langchain>=1.2,<2",
|
|
15
|
+
"langchain-openai>=1.2,<2",
|
|
16
|
+
"langgraph>=1.1,<2",
|
|
17
|
+
"langgraph-checkpoint>=4,<5",
|
|
18
|
+
"langgraph-prebuilt>=1.0,<2",
|
|
19
|
+
"langgraph-sdk>=0.3,<1",
|
|
20
|
+
"langgraph-checkpoint-postgres>=3.0,<4",
|
|
21
|
+
"langsmith>=0.4,<1",
|
|
22
|
+
"licos-platform-sdk>=0.1.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
licos-agent-runtime = "licos_agent_runtime.__main__:main"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/licos_agent_runtime"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""LICOS agent runtime.
|
|
2
|
+
|
|
3
|
+
The top-level package intentionally avoids importing FastAPI/LangGraph-heavy
|
|
4
|
+
modules. Import ``create_app`` or ``GraphService`` explicitly when starting a
|
|
5
|
+
runtime process.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .context import Context, RuntimeIdentity, new_context, request_context
|
|
9
|
+
from .graph_loader import is_agent_project, is_workflow_project
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Context",
|
|
13
|
+
"RuntimeIdentity",
|
|
14
|
+
"is_agent_project",
|
|
15
|
+
"is_workflow_project",
|
|
16
|
+
"new_context",
|
|
17
|
+
"request_context",
|
|
18
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import uvicorn
|
|
10
|
+
|
|
11
|
+
from .context import new_context
|
|
12
|
+
from .graph_loader import is_dev_env
|
|
13
|
+
from .logging import setup_logging
|
|
14
|
+
from .service import GraphService
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_input(input_str: str) -> dict[str, Any]:
|
|
18
|
+
if not input_str:
|
|
19
|
+
return {"text": "你好"}
|
|
20
|
+
try:
|
|
21
|
+
value = json.loads(input_str)
|
|
22
|
+
except json.JSONDecodeError:
|
|
23
|
+
return {"text": input_str}
|
|
24
|
+
return value if isinstance(value, dict) else {"input": value}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_args() -> argparse.Namespace:
|
|
28
|
+
parser = argparse.ArgumentParser(description="Start LICOS agent runtime")
|
|
29
|
+
parser.add_argument("-m", "--mode", default="http", choices=["http", "flow", "node", "agent"])
|
|
30
|
+
parser.add_argument("-n", "--node", default="", help="Node ID for node mode")
|
|
31
|
+
parser.add_argument("-p", "--port", type=int, default=5000, help="HTTP server port")
|
|
32
|
+
parser.add_argument("-i", "--input", default="", help="Input JSON string or plain text")
|
|
33
|
+
parser.add_argument("--host", default="0.0.0.0", help="HTTP bind host")
|
|
34
|
+
return parser.parse_args()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def start_http_server(host: str, port: int) -> None:
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
reload_enabled = is_dev_env()
|
|
40
|
+
logger.info("Start LICOS agent runtime HTTP server: host=%s port=%s reload=%s", host, port, reload_enabled)
|
|
41
|
+
uvicorn.run("licos_agent_runtime.app:app", host=host, port=port, reload=reload_enabled, workers=1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> None:
|
|
45
|
+
setup_logging()
|
|
46
|
+
args = parse_args()
|
|
47
|
+
service = GraphService()
|
|
48
|
+
|
|
49
|
+
if args.mode == "http":
|
|
50
|
+
start_http_server(args.host, args.port)
|
|
51
|
+
elif args.mode == "flow":
|
|
52
|
+
payload = parse_input(args.input)
|
|
53
|
+
result = asyncio.run(service.run(payload))
|
|
54
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
55
|
+
elif args.mode == "node":
|
|
56
|
+
if not args.node:
|
|
57
|
+
raise SystemExit("--node is required in node mode")
|
|
58
|
+
payload = parse_input(args.input)
|
|
59
|
+
result = asyncio.run(service.run_node(args.node, payload))
|
|
60
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
61
|
+
elif args.mode == "agent":
|
|
62
|
+
ctx = new_context(method="agent")
|
|
63
|
+
payload = parse_input(args.input)
|
|
64
|
+
for chunk in service.stream(payload, run_config={"configurable": {"session_id": ctx.run_id}}, ctx=ctx):
|
|
65
|
+
print(chunk)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
main()
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
9
|
+
from fastapi.responses import StreamingResponse
|
|
10
|
+
|
|
11
|
+
from . import telemetry
|
|
12
|
+
from .context import new_context, request_context
|
|
13
|
+
from .errors import extract_core_stack
|
|
14
|
+
from .graph_loader import is_agent_project
|
|
15
|
+
from .openai import OpenAIChatHandler
|
|
16
|
+
from .service import GraphService
|
|
17
|
+
from .stream_runner import RunOpt, agent_stream_handler, workflow_stream_handler
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
HEADER_X_RUN_ID = "x-run-id"
|
|
22
|
+
HEADER_X_WORKFLOW_STREAM_MODE = "x-workflow-stream-mode"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_app(service: GraphService | None = None) -> FastAPI:
|
|
26
|
+
runtime_service = service or GraphService()
|
|
27
|
+
openai_handler = OpenAIChatHandler(runtime_service)
|
|
28
|
+
app = FastAPI()
|
|
29
|
+
|
|
30
|
+
def register_task(run_id: str, task: asyncio.Task) -> None:
|
|
31
|
+
runtime_service.running_tasks[run_id] = task
|
|
32
|
+
|
|
33
|
+
@app.post("/run")
|
|
34
|
+
async def http_run(request: Request) -> dict[str, Any]:
|
|
35
|
+
raw_body = await request.body()
|
|
36
|
+
try:
|
|
37
|
+
body_text = raw_body.decode("utf-8")
|
|
38
|
+
except UnicodeDecodeError:
|
|
39
|
+
body_text = str(raw_body)
|
|
40
|
+
|
|
41
|
+
ctx = new_context(method="run", headers=request.headers)
|
|
42
|
+
request_context.set(ctx)
|
|
43
|
+
logger.info(
|
|
44
|
+
"Received /run: run_id=%s query=%s body=%s",
|
|
45
|
+
ctx.run_id,
|
|
46
|
+
dict(request.query_params),
|
|
47
|
+
body_text,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
payload = json.loads(body_text) if body_text else {}
|
|
52
|
+
return await runtime_service.run_with_timeout(payload, ctx)
|
|
53
|
+
except json.JSONDecodeError as exc:
|
|
54
|
+
logger.error("JSON decode error in /run: %s", exc)
|
|
55
|
+
raise HTTPException(status_code=400, detail=f"Invalid JSON format: {extract_core_stack()}") from exc
|
|
56
|
+
except asyncio.CancelledError:
|
|
57
|
+
return {"status": "cancelled", "run_id": ctx.run_id, "message": "Execution was cancelled"}
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
error_response = runtime_service.error_classifier.get_error_response(
|
|
60
|
+
exc,
|
|
61
|
+
{"node_name": "http_run", "run_id": ctx.run_id},
|
|
62
|
+
)
|
|
63
|
+
logger.error("Unexpected error in /run: %s", error_response, exc_info=True)
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=500,
|
|
66
|
+
detail={
|
|
67
|
+
"error_code": error_response["error_code"],
|
|
68
|
+
"error_message": error_response["error_message"],
|
|
69
|
+
"stack_trace": extract_core_stack(),
|
|
70
|
+
},
|
|
71
|
+
) from exc
|
|
72
|
+
finally:
|
|
73
|
+
telemetry.flush()
|
|
74
|
+
|
|
75
|
+
@app.post("/stream_run")
|
|
76
|
+
async def http_stream_run(request: Request) -> StreamingResponse:
|
|
77
|
+
ctx = new_context(method="stream_run", headers=request.headers)
|
|
78
|
+
request_context.set(ctx)
|
|
79
|
+
workflow_stream_mode = request.headers.get(HEADER_X_WORKFLOW_STREAM_MODE, "").lower()
|
|
80
|
+
workflow_debug = workflow_stream_mode == "debug"
|
|
81
|
+
stream_mode = None if workflow_debug else (workflow_stream_mode or None)
|
|
82
|
+
run_opt = RunOpt(workflow_debug=workflow_debug, stream_mode=stream_mode, platform_stream_protocol=True)
|
|
83
|
+
raw_body = await request.body()
|
|
84
|
+
try:
|
|
85
|
+
body_text = raw_body.decode("utf-8")
|
|
86
|
+
except UnicodeDecodeError:
|
|
87
|
+
body_text = str(raw_body)
|
|
88
|
+
logger.info(
|
|
89
|
+
"Received /stream_run: run_id=%s is_agent_project=%s query=%s body=%s",
|
|
90
|
+
ctx.run_id,
|
|
91
|
+
is_agent_project(),
|
|
92
|
+
dict(request.query_params),
|
|
93
|
+
body_text,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
payload = json.loads(body_text) if body_text else {}
|
|
98
|
+
except json.JSONDecodeError as exc:
|
|
99
|
+
logger.error("JSON decode error in /stream_run: %s", exc)
|
|
100
|
+
raise HTTPException(status_code=400, detail=f"Invalid JSON format: {extract_core_stack()}") from exc
|
|
101
|
+
|
|
102
|
+
if is_agent_project():
|
|
103
|
+
stream_generator = agent_stream_handler(
|
|
104
|
+
payload=payload,
|
|
105
|
+
ctx=ctx,
|
|
106
|
+
run_id=ctx.run_id,
|
|
107
|
+
stream_sse_func=runtime_service.stream_sse,
|
|
108
|
+
sse_event_func=runtime_service._sse_event,
|
|
109
|
+
error_classifier=runtime_service.error_classifier,
|
|
110
|
+
register_task_func=register_task,
|
|
111
|
+
run_opt=run_opt,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
stream_generator = workflow_stream_handler(
|
|
115
|
+
payload=payload,
|
|
116
|
+
ctx=ctx,
|
|
117
|
+
run_id=ctx.run_id,
|
|
118
|
+
stream_sse_func=runtime_service.stream_sse,
|
|
119
|
+
sse_event_func=runtime_service._sse_event,
|
|
120
|
+
error_classifier=runtime_service.error_classifier,
|
|
121
|
+
register_task_func=register_task,
|
|
122
|
+
run_opt=run_opt,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return StreamingResponse(stream_generator, media_type="text/event-stream")
|
|
126
|
+
|
|
127
|
+
@app.post("/cancel/{run_id}")
|
|
128
|
+
async def http_cancel(run_id: str, request: Request) -> dict[str, Any]:
|
|
129
|
+
ctx = new_context(method="cancel", headers=request.headers)
|
|
130
|
+
request_context.set(ctx)
|
|
131
|
+
return runtime_service.cancel_run(run_id, ctx)
|
|
132
|
+
|
|
133
|
+
@app.post("/node_run/{node_id}")
|
|
134
|
+
async def http_node_run(node_id: str, request: Request) -> Any:
|
|
135
|
+
raw_body = await request.body()
|
|
136
|
+
try:
|
|
137
|
+
body_text = raw_body.decode("utf-8")
|
|
138
|
+
except UnicodeDecodeError:
|
|
139
|
+
body_text = str(raw_body)
|
|
140
|
+
ctx = new_context(method="node_run", headers=request.headers)
|
|
141
|
+
request_context.set(ctx)
|
|
142
|
+
try:
|
|
143
|
+
payload = json.loads(body_text) if body_text else {}
|
|
144
|
+
return await runtime_service.run_node(node_id, payload, ctx)
|
|
145
|
+
except json.JSONDecodeError as exc:
|
|
146
|
+
raise HTTPException(status_code=400, detail=f"Invalid JSON format: {extract_core_stack()}") from exc
|
|
147
|
+
except KeyError as exc:
|
|
148
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
error_response = runtime_service.error_classifier.get_error_response(exc, {"node_name": node_id})
|
|
151
|
+
logger.error("Unexpected error in /node_run/%s: %s", node_id, error_response, exc_info=True)
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=500,
|
|
154
|
+
detail={
|
|
155
|
+
"error_code": error_response["error_code"],
|
|
156
|
+
"error_message": error_response["error_message"],
|
|
157
|
+
"stack_trace": extract_core_stack(),
|
|
158
|
+
},
|
|
159
|
+
) from exc
|
|
160
|
+
finally:
|
|
161
|
+
telemetry.flush()
|
|
162
|
+
|
|
163
|
+
@app.post("/v1/chat/completions")
|
|
164
|
+
async def openai_chat_completions(request: Request) -> Any:
|
|
165
|
+
ctx = new_context(method="openai_chat", headers=request.headers)
|
|
166
|
+
request_context.set(ctx)
|
|
167
|
+
try:
|
|
168
|
+
payload = await request.json()
|
|
169
|
+
return await openai_handler.handle(payload, ctx)
|
|
170
|
+
except json.JSONDecodeError as exc:
|
|
171
|
+
raise HTTPException(status_code=400, detail="Invalid JSON format") from exc
|
|
172
|
+
finally:
|
|
173
|
+
telemetry.flush()
|
|
174
|
+
|
|
175
|
+
@app.get("/health")
|
|
176
|
+
async def health_check() -> dict[str, str]:
|
|
177
|
+
return {"status": "ok", "message": "Service is running"}
|
|
178
|
+
|
|
179
|
+
@app.get("/graph_parameter")
|
|
180
|
+
async def http_graph_inout_parameter(request: Request) -> dict[str, Any]:
|
|
181
|
+
del request
|
|
182
|
+
return runtime_service.graph_inout_schema()
|
|
183
|
+
|
|
184
|
+
return app
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
app = create_app()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _env(name: str) -> str | None:
|
|
11
|
+
value = os.environ.get(name)
|
|
12
|
+
if value is None:
|
|
13
|
+
return None
|
|
14
|
+
value = value.strip()
|
|
15
|
+
return value or None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _headers_to_dict(headers: Mapping[str, Any] | None) -> dict[str, str]:
|
|
19
|
+
if headers is None:
|
|
20
|
+
return {}
|
|
21
|
+
return {str(key).lower(): str(value) for key, value in headers.items()}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class RuntimeIdentity:
|
|
26
|
+
user_id: str | None = None
|
|
27
|
+
workspace_id: str | None = None
|
|
28
|
+
project_id: str | None = None
|
|
29
|
+
project_type: str | None = None
|
|
30
|
+
instance_id: str | None = None
|
|
31
|
+
profile_id: str | None = None
|
|
32
|
+
user_token: str | None = None
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_env(cls) -> "RuntimeIdentity":
|
|
36
|
+
return cls(
|
|
37
|
+
user_id=_env("AGENT_USER_ID") or _env("LICOS_USER_ID"),
|
|
38
|
+
workspace_id=_env("AGENT_WORKSPACE_ID") or _env("LICOS_WORKSPACE_ID"),
|
|
39
|
+
project_id=_env("AGENT_PROJECT_ID") or _env("LICOS_PROJECT_ID"),
|
|
40
|
+
project_type=_env("AGENT_PROJECT_TYPE") or _env("LICOS_PROJECT_TYPE"),
|
|
41
|
+
instance_id=_env("INSTANCE_ID") or _env("LICOS_INSTANCE_ID"),
|
|
42
|
+
profile_id=_env("AGENT_PROFILE_ID") or _env("LICOS_AGENT_PROFILE_ID"),
|
|
43
|
+
user_token=_env("LICOS_USER_TOKEN"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def to_headers(self) -> dict[str, str]:
|
|
47
|
+
headers: dict[str, str] = {}
|
|
48
|
+
if self.user_id:
|
|
49
|
+
headers["X-User-Id"] = self.user_id
|
|
50
|
+
if self.workspace_id:
|
|
51
|
+
headers["X-Workspace-Id"] = self.workspace_id
|
|
52
|
+
if self.project_id:
|
|
53
|
+
headers["X-Project-Id"] = self.project_id
|
|
54
|
+
if self.user_token:
|
|
55
|
+
headers["Authorization"] = f"Bearer {self.user_token}"
|
|
56
|
+
return headers
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(slots=True)
|
|
60
|
+
class Context:
|
|
61
|
+
method: str
|
|
62
|
+
run_id: str = field(default_factory=lambda: uuid4().hex)
|
|
63
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
64
|
+
identity: RuntimeIdentity = field(default_factory=RuntimeIdentity.from_env)
|
|
65
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
def header(self, name: str, default: str | None = None) -> str | None:
|
|
68
|
+
return self.headers.get(name.lower(), default)
|
|
69
|
+
|
|
70
|
+
def with_run_id(self, run_id: str | None) -> "Context":
|
|
71
|
+
if run_id and run_id.strip():
|
|
72
|
+
self.run_id = run_id.strip()
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
request_context: ContextVar[Context | None] = ContextVar(
|
|
77
|
+
"licos_agent_request_context",
|
|
78
|
+
default=None,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def new_context(
|
|
83
|
+
method: str = "run",
|
|
84
|
+
headers: Mapping[str, Any] | None = None,
|
|
85
|
+
**metadata: Any,
|
|
86
|
+
) -> Context:
|
|
87
|
+
ctx = Context(method=method, headers=_headers_to_dict(headers), metadata=dict(metadata))
|
|
88
|
+
upstream_run_id = ctx.header("x-run-id")
|
|
89
|
+
if upstream_run_id:
|
|
90
|
+
ctx.run_id = upstream_run_id
|
|
91
|
+
return ctx
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import traceback
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ErrorCategory(str, Enum):
|
|
10
|
+
CANCELLED = "cancelled"
|
|
11
|
+
TIMEOUT = "timeout"
|
|
12
|
+
VALIDATION = "validation"
|
|
13
|
+
RUNTIME = "runtime"
|
|
14
|
+
INTERNAL = "internal"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class ClassifiedError:
|
|
19
|
+
code: str
|
|
20
|
+
message: str
|
|
21
|
+
category: ErrorCategory
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ErrorClassifier:
|
|
25
|
+
def classify(self, error: BaseException, context: dict[str, Any] | None = None) -> ClassifiedError:
|
|
26
|
+
del context
|
|
27
|
+
name = error.__class__.__name__
|
|
28
|
+
if name == "CancelledError":
|
|
29
|
+
return ClassifiedError("RUN_CANCELLED", str(error) or "Execution cancelled", ErrorCategory.CANCELLED)
|
|
30
|
+
if name == "TimeoutError":
|
|
31
|
+
return ClassifiedError("RUN_TIMEOUT", str(error) or "Execution timeout", ErrorCategory.TIMEOUT)
|
|
32
|
+
if isinstance(error, (KeyError, ValueError, TypeError)):
|
|
33
|
+
return ClassifiedError("PARAM_INVALID", str(error), ErrorCategory.VALIDATION)
|
|
34
|
+
return ClassifiedError("RUNTIME_ERROR", str(error) or name, ErrorCategory.RUNTIME)
|
|
35
|
+
|
|
36
|
+
def get_error_response(
|
|
37
|
+
self,
|
|
38
|
+
error: BaseException,
|
|
39
|
+
context: dict[str, Any] | None = None,
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
classified = self.classify(error, context)
|
|
42
|
+
return {
|
|
43
|
+
"error_code": classified.code,
|
|
44
|
+
"error_message": classified.message,
|
|
45
|
+
"category": classified.category.value,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_core_stack() -> str:
|
|
50
|
+
return "".join(traceback.format_exc())
|