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.
Files changed (25) hide show
  1. licos_agent_runtime-0.1.0/.gitignore +44 -0
  2. licos_agent_runtime-0.1.0/PKG-INFO +17 -0
  3. licos_agent_runtime-0.1.0/pyproject.toml +29 -0
  4. licos_agent_runtime-0.1.0/src/licos_agent_runtime/__init__.py +18 -0
  5. licos_agent_runtime-0.1.0/src/licos_agent_runtime/__main__.py +69 -0
  6. licos_agent_runtime-0.1.0/src/licos_agent_runtime/app.py +187 -0
  7. licos_agent_runtime-0.1.0/src/licos_agent_runtime/context.py +91 -0
  8. licos_agent_runtime-0.1.0/src/licos_agent_runtime/errors.py +50 -0
  9. licos_agent_runtime-0.1.0/src/licos_agent_runtime/graph_loader.py +296 -0
  10. licos_agent_runtime-0.1.0/src/licos_agent_runtime/logging.py +44 -0
  11. licos_agent_runtime-0.1.0/src/licos_agent_runtime/openai.py +98 -0
  12. licos_agent_runtime-0.1.0/src/licos_agent_runtime/parser.py +63 -0
  13. licos_agent_runtime-0.1.0/src/licos_agent_runtime/payload.py +132 -0
  14. licos_agent_runtime-0.1.0/src/licos_agent_runtime/run_config.py +28 -0
  15. licos_agent_runtime-0.1.0/src/licos_agent_runtime/service.py +322 -0
  16. licos_agent_runtime-0.1.0/src/licos_agent_runtime/stream_protocol.py +373 -0
  17. licos_agent_runtime-0.1.0/src/licos_agent_runtime/stream_runner.py +241 -0
  18. licos_agent_runtime-0.1.0/src/licos_agent_runtime/telemetry.py +22 -0
  19. licos_agent_runtime-0.1.0/tests/test_context.py +34 -0
  20. licos_agent_runtime-0.1.0/tests/test_graph_loader.py +118 -0
  21. licos_agent_runtime-0.1.0/tests/test_parser.py +40 -0
  22. licos_agent_runtime-0.1.0/tests/test_payload.py +35 -0
  23. licos_agent_runtime-0.1.0/tests/test_service_stream_protocol.py +59 -0
  24. licos_agent_runtime-0.1.0/tests/test_stream_protocol.py +73 -0
  25. 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())