union-app-chat-stream 1.1.6

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 (66) hide show
  1. package/app/__init__.py +1 -0
  2. package/app/agent/__init__.py +1 -0
  3. package/app/agent/capabilities.py +388 -0
  4. package/app/agent/coordinator/__init__.py +1 -0
  5. package/app/agent/coordinator/definition.py +50 -0
  6. package/app/agent/coordinator/output_guard.py +29 -0
  7. package/app/agent/graph.py +95 -0
  8. package/app/agent/guardrails.py +30 -0
  9. package/app/agent/routing.py +81 -0
  10. package/app/agent/runtime/__init__.py +1 -0
  11. package/app/agent/runtime/activity.py +393 -0
  12. package/app/agent/runtime/delegation.py +80 -0
  13. package/app/agent/runtime/deps.py +34 -0
  14. package/app/agent/runtime/execution.py +368 -0
  15. package/app/agent/runtime/model.py +47 -0
  16. package/app/agent/runtime/model_errors.py +24 -0
  17. package/app/agent/runtime/session.py +154 -0
  18. package/app/agent/specialists/__init__.py +1 -0
  19. package/app/agent/specialists/behavior_risk/__init__.py +1 -0
  20. package/app/agent/specialists/behavior_risk/definition.py +54 -0
  21. package/app/agent/specialists/build.py +94 -0
  22. package/app/agent/specialists/knowledge/__init__.py +1 -0
  23. package/app/agent/specialists/knowledge/definition.py +38 -0
  24. package/app/agent/specialists/personal_memory/__init__.py +1 -0
  25. package/app/agent/specialists/personal_memory/definition.py +35 -0
  26. package/app/agent/specialists/personal_memory/output_guard.py +55 -0
  27. package/app/agent/specialists/running_analysis/__init__.py +1 -0
  28. package/app/agent/specialists/running_analysis/definition.py +46 -0
  29. package/app/agent/specialists/running_analysis/output_guard.py +38 -0
  30. package/app/agent/specialists/scheduled_task_draft/__init__.py +8 -0
  31. package/app/agent/specialists/scheduled_task_draft/definition.py +142 -0
  32. package/app/agent/specialists/scheduled_task_draft/output_guard.py +81 -0
  33. package/app/asgi.py +139 -0
  34. package/app/config/__init__.py +1 -0
  35. package/app/config/settings.py +67 -0
  36. package/app/memory/__init__.py +1 -0
  37. package/app/memory/store.py +154 -0
  38. package/app/service/rag_service.py +364 -0
  39. package/app/skills/full-chain-quality-analysis/SKILL.md +22 -0
  40. package/app/tools/__init__.py +1 -0
  41. package/app/tools/business.py +183 -0
  42. package/app/utils/__init__.py +1 -0
  43. package/app/utils/api_client.py +76 -0
  44. package/app/utils/control_auth.py +35 -0
  45. package/app/utils/state_client.py +60 -0
  46. package/app/views/__init__.py +1 -0
  47. package/app/views/auth.py +189 -0
  48. package/app/views/errors.py +19 -0
  49. package/app/views/routes.py +25 -0
  50. package/app/views/run_context.py +33 -0
  51. package/app/views/streaming_runs.py +340 -0
  52. package/app/views/sync_runs.py +152 -0
  53. package/deploy/autoconf/templates/env.j2 +23 -0
  54. package/deploy/autoconf.yml +15 -0
  55. package/deploy/scripts/healthcheck.sh +12 -0
  56. package/deploy/scripts/start.sh +80 -0
  57. package/deploy/scripts/stop.sh +35 -0
  58. package/knowledge/000036-scenario-offline-function-call-mock-v1.md +134 -0
  59. package/package.json +21 -0
  60. package/requirements.txt +10 -0
  61. package/scripts/healthcheck.sh +4 -0
  62. package/scripts/start-BJ11.sh +1 -0
  63. package/scripts/start-BJ12.sh +1 -0
  64. package/scripts/start-SH20.sh +1 -0
  65. package/scripts/start-SZ31.sh +1 -0
  66. package/scripts/stop.sh +4 -0
package/app/asgi.py ADDED
@@ -0,0 +1,139 @@
1
+ """创建 Starlette ASGI 应用并组装配置、运行时与 HTTP 路由。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from contextlib import asynccontextmanager
8
+ from functools import lru_cache
9
+ from typing import Any
10
+
11
+ import httpx
12
+ import logfire
13
+ from dotenv import load_dotenv
14
+ from starlette.applications import Starlette
15
+
16
+ from app.agent.graph import AgentGraph, build_agent_graph
17
+ from app.agent.runtime.execution import ExecutionCoordinator
18
+ from app.agent.runtime.model import build_model
19
+ from app.config.settings import AgentSettings
20
+ from app.views.routes import RouteError, build_routes, http_error
21
+
22
+
23
+ @lru_cache(maxsize=1)
24
+ def _configure_observability(environment: str) -> None:
25
+ level = getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO)
26
+ logging.basicConfig(
27
+ level=level,
28
+ format="%(asctime)s %(levelname)s %(name)s %(message)s",
29
+ )
30
+ logfire.configure(
31
+ send_to_logfire=False,
32
+ service_name="union-py-app",
33
+ environment=environment,
34
+ inspect_arguments=False,
35
+ )
36
+
37
+
38
+ class Runtime:
39
+ def __init__(
40
+ self,
41
+ *,
42
+ settings: AgentSettings,
43
+ http: httpx.AsyncClient,
44
+ graph: AgentGraph,
45
+ rag_service: Any | None,
46
+ ) -> None:
47
+ self.settings = settings
48
+ self.http = http
49
+ self.graph = graph
50
+ self.rag_service = rag_service
51
+ self.executions = ExecutionCoordinator(
52
+ max_run_seconds=settings.max_run_seconds,
53
+ )
54
+
55
+
56
+ def _rag_service(settings: AgentSettings):
57
+ if not settings.rag_enabled:
58
+ return None
59
+ from app.service.rag_service import RagService
60
+
61
+ return RagService(
62
+ {
63
+ "RAG_ENABLED": settings.rag_enabled,
64
+ "RAG_TOP_K": settings.rag_top_k,
65
+ "RAG_KNOWLEDGE_DIR": settings.rag_knowledge_dir,
66
+ "RAG_COLLECTION": settings.rag_collection,
67
+ "LLM_URL": settings.llm_url,
68
+ "LLM_KEY": settings.llm_key,
69
+ "RAG_EMBEDDING_MODEL": settings.rag_embedding_model,
70
+ "RAG_CHUNK_SIZE": settings.rag_chunk_size,
71
+ }
72
+ )
73
+
74
+
75
+ def create_app(
76
+ *,
77
+ settings: AgentSettings | None = None,
78
+ model: Any | None = None,
79
+ http: httpx.AsyncClient | None = None,
80
+ rag_service: Any | None = None,
81
+ ) -> Starlette:
82
+ environment = os.getenv("APP_ENV", "dev")
83
+ if settings is None:
84
+ load_dotenv(
85
+ os.path.join(
86
+ os.path.dirname(__file__),
87
+ "config",
88
+ "env",
89
+ f".env.{environment}",
90
+ ),
91
+ override=False,
92
+ )
93
+ _configure_observability(environment)
94
+ configured = settings or AgentSettings.from_env()
95
+
96
+ @asynccontextmanager
97
+ async def lifespan(app: Starlette):
98
+ shared_http = http or httpx.AsyncClient(
99
+ limits=httpx.Limits(
100
+ max_connections=100,
101
+ max_keepalive_connections=20,
102
+ ),
103
+ follow_redirects=False,
104
+ )
105
+ agent_model = model or build_model(configured, shared_http)
106
+ graph = build_agent_graph(
107
+ agent_model,
108
+ configured,
109
+ router_model=(
110
+ model or build_model(configured, shared_http, thinking=False)
111
+ ),
112
+ )
113
+ runtime = Runtime(
114
+ settings=configured,
115
+ http=shared_http,
116
+ graph=graph,
117
+ rag_service=(
118
+ rag_service
119
+ if rag_service is not None
120
+ else _rag_service(configured)
121
+ ),
122
+ )
123
+ app.state.runtime = runtime
124
+ await runtime.executions.startup()
125
+ try:
126
+ yield
127
+ finally:
128
+ await runtime.executions.close()
129
+ if http is None:
130
+ await shared_http.aclose()
131
+
132
+ return Starlette(
133
+ lifespan=lifespan,
134
+ routes=build_routes(),
135
+ exception_handlers={RouteError: http_error},
136
+ )
137
+
138
+
139
+ app = create_app()
@@ -0,0 +1 @@
1
+ """存放应用配置模型及环境配置文件。"""
@@ -0,0 +1,67 @@
1
+ """读取并校验应用运行所需的环境变量配置。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+
8
+
9
+ def _integer(name: str, default: int) -> int:
10
+ try:
11
+ return int(os.getenv(name, str(default)))
12
+ except ValueError:
13
+ return default
14
+
15
+
16
+ def _floating(name: str, default: float) -> float:
17
+ try:
18
+ return float(os.getenv(name, str(default)))
19
+ except ValueError:
20
+ return default
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AgentSettings:
25
+ union_base_url: str
26
+ llm_url: str
27
+ llm_key: str
28
+ llm_model: str
29
+ llm_context_window: int
30
+ request_limit: int
31
+ tool_calls_limit: int
32
+ input_tokens_limit: int
33
+ output_tokens_limit: int
34
+ tool_timeout_seconds: float
35
+ subagent_timeout_seconds: float
36
+ max_run_seconds: float
37
+ required_permission: str
38
+ rag_enabled: bool
39
+ rag_knowledge_dir: str
40
+ rag_collection: str
41
+ rag_embedding_model: str
42
+ rag_top_k: int
43
+ rag_chunk_size: int
44
+
45
+ @classmethod
46
+ def from_env(cls) -> AgentSettings:
47
+ return cls(
48
+ union_base_url=os.getenv("UNION_BASE_URL", "http://127.0.0.1:8080").rstrip("/"),
49
+ llm_url=os.getenv("LLM_URL", "").rstrip("/"),
50
+ llm_key=os.getenv("LLM_KEY", ""),
51
+ llm_model=os.getenv("LLM_MODEL", ""),
52
+ llm_context_window=_integer("LLM_CONTEXT_WINDOW", 131072),
53
+ request_limit=_integer("AGENT_REQUEST_LIMIT", 30),
54
+ tool_calls_limit=_integer("AGENT_TOOL_CALLS_LIMIT", 40),
55
+ input_tokens_limit=_integer("AGENT_INPUT_TOKENS_LIMIT", 120000),
56
+ output_tokens_limit=_integer("AGENT_OUTPUT_TOKENS_LIMIT", 16000),
57
+ tool_timeout_seconds=_floating("AGENT_TOOL_TIMEOUT_SECONDS", 300.0),
58
+ subagent_timeout_seconds=_floating("SUBAGENT_TIMEOUT_SECONDS", 900.0),
59
+ max_run_seconds=_floating("AGENT_MAX_RUN_SECONDS", 900.0),
60
+ required_permission=os.getenv("PERMISSIONS", ""),
61
+ rag_enabled=os.getenv("RAG_ENABLED", "true").lower() in {"1", "true", "yes", "on"},
62
+ rag_knowledge_dir=os.getenv("RAG_KNOWLEDGE_DIR", "knowledge"),
63
+ rag_collection=os.getenv("RAG_COLLECTION", "ops_knowledge"),
64
+ rag_embedding_model=os.getenv("RAG_EMBEDDING_MODEL", "embedding-3"),
65
+ rag_top_k=_integer("RAG_TOP_K", 5),
66
+ rag_chunk_size=_integer("RAG_CHUNK_SIZE", 1200),
67
+ )
@@ -0,0 +1 @@
1
+ """存放 Agent 个人记忆的存储适配实现。"""
@@ -0,0 +1,154 @@
1
+ """将远端记忆接口适配为 Harness 记忆存储协议。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic_ai_harness.memory import (
8
+ MemoryConflictError,
9
+ MemoryFile,
10
+ MemoryMutation,
11
+ MemoryOperation,
12
+ MemoryOperationConflictError,
13
+ MemorySearchMatch,
14
+ MemorySearchResult,
15
+ )
16
+
17
+ from app.utils.state_client import AgentStateClient, AgentStateError
18
+
19
+
20
+ class AgentMemoryStore:
21
+ """HTTP adapter for the official SearchableMemoryStore protocol."""
22
+
23
+ def __init__(self, state_client: AgentStateClient) -> None:
24
+ self._state_client = state_client
25
+
26
+ async def _call(self, action: str, payload: dict[str, Any]) -> dict[str, Any]:
27
+ body = await self._state_client.post(f"/agent/memoryStore/{action}", payload)
28
+ code = body.get("errorCode")
29
+ if code == "version_conflict":
30
+ raise MemoryConflictError(body.get("errorMsg", "memory version conflict"))
31
+ if code == "operation_conflict":
32
+ raise MemoryOperationConflictError(body.get("errorMsg", "memory operation conflict"))
33
+ if body.get("success") is False:
34
+ raise AgentStateError(body.get("errorMsg", "memory store request failed"))
35
+ return body
36
+
37
+ async def read(self, path: str, *, max_chars: int) -> MemoryFile | None:
38
+ body = await self._call("read", {"path": path, "maxChars": max_chars})
39
+ item = body.get("file")
40
+ if item is None:
41
+ return None
42
+ return MemoryFile(
43
+ content=str(item["content"]),
44
+ version=str(item["version"]),
45
+ operation_id=item.get("operationId"),
46
+ truncated=bool(item.get("truncated", False)),
47
+ )
48
+
49
+ async def list_paths(self, prefix: str = "", *, limit: int) -> list[str]:
50
+ body = await self._call("list", {"prefix": prefix, "limit": limit})
51
+ return [str(path) for path in body.get("paths", [])]
52
+
53
+ async def get_operation(self, operation: MemoryOperation) -> MemoryMutation | None:
54
+ body = await self._call(
55
+ "operation",
56
+ {"operationId": operation.id, "fingerprint": operation.fingerprint},
57
+ )
58
+ return self._mutation(body.get("mutation"))
59
+
60
+ async def write(
61
+ self,
62
+ path: str,
63
+ content: str,
64
+ *,
65
+ expected_version: str | None,
66
+ operation: MemoryOperation | None = None,
67
+ ) -> MemoryMutation:
68
+ return self._required_mutation(
69
+ await self._call(
70
+ "write",
71
+ {
72
+ "path": path,
73
+ "content": content,
74
+ "expectedVersion": expected_version,
75
+ "operation": self._operation(operation),
76
+ },
77
+ )
78
+ )
79
+
80
+ async def delete(
81
+ self,
82
+ path: str,
83
+ *,
84
+ expected_version: str | None,
85
+ operation: MemoryOperation | None = None,
86
+ ) -> MemoryMutation:
87
+ return self._required_mutation(
88
+ await self._call(
89
+ "delete",
90
+ {
91
+ "path": path,
92
+ "expectedVersion": expected_version,
93
+ "operation": self._operation(operation),
94
+ },
95
+ )
96
+ )
97
+
98
+ async def search(
99
+ self,
100
+ prefix: str,
101
+ query: str,
102
+ *,
103
+ limit: int,
104
+ max_files: int,
105
+ max_chars: int,
106
+ max_file_chars: int,
107
+ ) -> MemorySearchResult:
108
+ body = await self._call(
109
+ "search",
110
+ {
111
+ "prefix": prefix,
112
+ "query": query,
113
+ "limit": limit,
114
+ "maxFiles": max_files,
115
+ "maxChars": max_chars,
116
+ "maxFileChars": max_file_chars,
117
+ },
118
+ )
119
+ result = body.get("result") or {}
120
+ return MemorySearchResult(
121
+ matches=[
122
+ MemorySearchMatch(
123
+ path=str(item["path"]),
124
+ snippet=str(item["snippet"]),
125
+ score=float(item["score"]),
126
+ )
127
+ for item in result.get("matches", [])
128
+ ],
129
+ scanned=int(result.get("scanned", 0)),
130
+ truncated=bool(result.get("truncated", False)),
131
+ )
132
+
133
+ @staticmethod
134
+ def _operation(operation: MemoryOperation | None) -> dict[str, str] | None:
135
+ if operation is None:
136
+ return None
137
+ return {"id": operation.id, "fingerprint": operation.fingerprint}
138
+
139
+ @staticmethod
140
+ def _mutation(value: Any) -> MemoryMutation | None:
141
+ if value is None:
142
+ return None
143
+ return MemoryMutation(
144
+ version=str(value["version"]) if value.get("version") is not None else None,
145
+ replayed=bool(value.get("replayed", False)),
146
+ existed=bool(value.get("existed", False)),
147
+ )
148
+
149
+ @classmethod
150
+ def _required_mutation(cls, body: dict[str, Any]) -> MemoryMutation:
151
+ mutation = cls._mutation(body.get("mutation"))
152
+ if mutation is None:
153
+ raise AgentStateError("state service omitted memory mutation result")
154
+ return mutation