@openlucaskaka/kagent 0.1.7
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.
- package/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- package/src/kagent/utils/paths.py +473 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import io
|
|
6
|
+
import time
|
|
7
|
+
import warnings
|
|
8
|
+
from typing import Any, Callable, Dict, List
|
|
9
|
+
|
|
10
|
+
from kagent.core.invariants import validate_run_invariants
|
|
11
|
+
from kagent.core.summary import summarize_run
|
|
12
|
+
from kagent.eval.cases import (
|
|
13
|
+
CaseCheck,
|
|
14
|
+
EvaluationCase,
|
|
15
|
+
build_evaluation_cases,
|
|
16
|
+
)
|
|
17
|
+
from kagent.utils.json_output import format_and_write_json
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def evaluate_agent(category: str = "", case_name: str = "") -> Dict[str, Any]:
|
|
21
|
+
started_at = time.perf_counter()
|
|
22
|
+
warning_sink = io.StringIO()
|
|
23
|
+
with contextlib.redirect_stderr(warning_sink), warnings.catch_warnings():
|
|
24
|
+
warnings.simplefilter("ignore")
|
|
25
|
+
from kagent.core.agent import AgentConfig, AgentStatus, run_agent
|
|
26
|
+
|
|
27
|
+
all_cases = build_evaluation_cases(AgentConfig, AgentStatus, run_agent)
|
|
28
|
+
_validate_case_filters(all_cases, category=category, case_name=case_name)
|
|
29
|
+
selected_cases = _filter_cases(
|
|
30
|
+
all_cases,
|
|
31
|
+
category=category,
|
|
32
|
+
case_name=case_name,
|
|
33
|
+
)
|
|
34
|
+
cases = [
|
|
35
|
+
_run_case(item.name, item.category, item.run, item.check)
|
|
36
|
+
for item in selected_cases
|
|
37
|
+
]
|
|
38
|
+
passed = sum(1 for case in cases if case["passed"])
|
|
39
|
+
recovered_cases = _recovered_case_count(cases)
|
|
40
|
+
return {
|
|
41
|
+
"passed": passed,
|
|
42
|
+
"failed": len(cases) - passed,
|
|
43
|
+
"filters": {"category": category, "case": case_name},
|
|
44
|
+
"recovered_cases": str(recovered_cases),
|
|
45
|
+
"recovery_rate": f"{recovered_cases / len(cases):.2f}" if cases else "0.00",
|
|
46
|
+
"category_counts": _category_counts(cases),
|
|
47
|
+
"duration_seconds": _duration_since(started_at),
|
|
48
|
+
"slowest_case": _slowest_case_name(cases),
|
|
49
|
+
"cases": cases,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def registered_evaluation_cases() -> List[Dict[str, str]]:
|
|
54
|
+
warning_sink = io.StringIO()
|
|
55
|
+
with contextlib.redirect_stderr(warning_sink), warnings.catch_warnings():
|
|
56
|
+
warnings.simplefilter("ignore")
|
|
57
|
+
from kagent.core.agent import AgentConfig, AgentStatus, run_agent
|
|
58
|
+
|
|
59
|
+
return [
|
|
60
|
+
{"name": case.name, "category": case.category}
|
|
61
|
+
for case in build_evaluation_cases(AgentConfig, AgentStatus, run_agent)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _validate_case_filters(
|
|
66
|
+
cases: List[EvaluationCase],
|
|
67
|
+
*,
|
|
68
|
+
category: str,
|
|
69
|
+
case_name: str,
|
|
70
|
+
) -> None:
|
|
71
|
+
if category and category not in {case.category for case in cases}:
|
|
72
|
+
raise ValueError(f"unknown evaluator category: {category}")
|
|
73
|
+
if case_name and case_name not in {case.name for case in cases}:
|
|
74
|
+
raise ValueError(f"unknown evaluator case: {case_name}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _filter_cases(
|
|
78
|
+
cases: List[EvaluationCase],
|
|
79
|
+
*,
|
|
80
|
+
category: str,
|
|
81
|
+
case_name: str,
|
|
82
|
+
) -> List[EvaluationCase]:
|
|
83
|
+
selected = cases
|
|
84
|
+
if category:
|
|
85
|
+
selected = [case for case in selected if case.category == category]
|
|
86
|
+
if case_name:
|
|
87
|
+
selected = [case for case in selected if case.name == case_name]
|
|
88
|
+
return selected
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _run_case(
|
|
92
|
+
name: str,
|
|
93
|
+
category: str,
|
|
94
|
+
run: Callable[[], Dict[str, Any]],
|
|
95
|
+
check: CaseCheck,
|
|
96
|
+
) -> Dict[str, Any]:
|
|
97
|
+
started_at = time.perf_counter()
|
|
98
|
+
try:
|
|
99
|
+
result = run()
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
return {
|
|
102
|
+
"name": name,
|
|
103
|
+
"category": category,
|
|
104
|
+
"passed": False,
|
|
105
|
+
"status": "error",
|
|
106
|
+
"answer": None,
|
|
107
|
+
"retry_count": 0,
|
|
108
|
+
"events": [],
|
|
109
|
+
"invariant_errors": [],
|
|
110
|
+
"summary": {},
|
|
111
|
+
"error": str(exc),
|
|
112
|
+
"duration_seconds": _duration_since(started_at),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
invariant_errors = validate_run_invariants(result)
|
|
116
|
+
return {
|
|
117
|
+
"name": name,
|
|
118
|
+
"category": category,
|
|
119
|
+
"passed": check(result) and not invariant_errors,
|
|
120
|
+
"status": result["status"].value,
|
|
121
|
+
"answer": result.get("answer"),
|
|
122
|
+
"retry_count": result.get("retry_count", 0),
|
|
123
|
+
"events": [event["node"] for event in result.get("events", [])],
|
|
124
|
+
"invariant_errors": invariant_errors,
|
|
125
|
+
"summary": summarize_run(result),
|
|
126
|
+
"error": "",
|
|
127
|
+
"duration_seconds": _duration_since(started_at),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _duration_since(started_at: float) -> str:
|
|
132
|
+
return f"{time.perf_counter() - started_at:.4f}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _slowest_case_name(cases: List[Dict[str, Any]]) -> str:
|
|
136
|
+
if not cases:
|
|
137
|
+
return ""
|
|
138
|
+
return max(cases, key=lambda case: float(case["duration_seconds"]))["name"]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _recovered_case_count(cases: List[Dict[str, Any]]) -> int:
|
|
142
|
+
return sum(
|
|
143
|
+
1
|
|
144
|
+
for case in cases
|
|
145
|
+
if case.get("summary", {}).get("recovered") == "true"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _category_counts(cases: List[Dict[str, Any]]) -> Dict[str, str]:
|
|
150
|
+
counts: Dict[str, int] = {}
|
|
151
|
+
for case in cases:
|
|
152
|
+
category = case.get("category", "")
|
|
153
|
+
counts[category] = counts.get(category, 0) + 1
|
|
154
|
+
return {category: str(counts[category]) for category in sorted(counts)}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _exit_code_for_report(report: Dict[str, Any], *, fail_on_failure: bool) -> int:
|
|
158
|
+
if fail_on_failure and int(report.get("failed", 0)) > 0:
|
|
159
|
+
return 1
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main() -> None:
|
|
164
|
+
parser = argparse.ArgumentParser(description="Evaluate the kagent.")
|
|
165
|
+
parser.add_argument(
|
|
166
|
+
"--category",
|
|
167
|
+
default="",
|
|
168
|
+
help="Run only evaluator cases in this category, such as recovery or tool.",
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--case",
|
|
172
|
+
default="",
|
|
173
|
+
help="Run only the evaluator case with this exact name.",
|
|
174
|
+
)
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--list-cases",
|
|
177
|
+
action="store_true",
|
|
178
|
+
help="List evaluator case names and categories without running them.",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--output",
|
|
182
|
+
default="",
|
|
183
|
+
metavar="PATH",
|
|
184
|
+
help="Write the JSON payload to PATH as well as stdout.",
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--fail-on-failure",
|
|
188
|
+
action="store_true",
|
|
189
|
+
help="Exit with code 1 when the evaluator report contains failed cases.",
|
|
190
|
+
)
|
|
191
|
+
args = parser.parse_args()
|
|
192
|
+
if args.list_cases:
|
|
193
|
+
_emit_json_payload({"cases": registered_evaluation_cases()}, args.output, parser)
|
|
194
|
+
return
|
|
195
|
+
try:
|
|
196
|
+
report = evaluate_agent(category=args.category, case_name=args.case)
|
|
197
|
+
except ValueError as exc:
|
|
198
|
+
parser.error(str(exc))
|
|
199
|
+
_emit_json_payload(report, args.output, parser)
|
|
200
|
+
raise SystemExit(_exit_code_for_report(report, fail_on_failure=args.fail_on_failure))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _emit_json_payload(
|
|
204
|
+
payload: Dict[str, Any],
|
|
205
|
+
output_path: str,
|
|
206
|
+
parser: argparse.ArgumentParser,
|
|
207
|
+
) -> None:
|
|
208
|
+
try:
|
|
209
|
+
json_payload = format_and_write_json(payload, output_path)
|
|
210
|
+
except OSError as exc:
|
|
211
|
+
parser.error(f"could not write --output file: {exc}")
|
|
212
|
+
print(json_payload)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
_AUDIT_EVENT_FIELDS = (
|
|
9
|
+
"run_id",
|
|
10
|
+
"type",
|
|
11
|
+
"node",
|
|
12
|
+
"iteration",
|
|
13
|
+
"action_id",
|
|
14
|
+
"tool",
|
|
15
|
+
"status",
|
|
16
|
+
"error_code",
|
|
17
|
+
"duration_seconds",
|
|
18
|
+
"action_count",
|
|
19
|
+
"iteration_count",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class KafkaRestAuditHook:
|
|
25
|
+
url: str
|
|
26
|
+
topic: str
|
|
27
|
+
timeout_seconds: float = 2.0
|
|
28
|
+
fail_closed: bool = False
|
|
29
|
+
failure_count: int = 0
|
|
30
|
+
|
|
31
|
+
def on_run_end(self, context: Dict[str, Any]) -> None:
|
|
32
|
+
payload = {
|
|
33
|
+
"topic": self.topic,
|
|
34
|
+
"event": {
|
|
35
|
+
"type": "run_end",
|
|
36
|
+
"run_id": str(context.get("run_id", "")),
|
|
37
|
+
"goal": str(context.get("goal", "")),
|
|
38
|
+
"status": str(context.get("status", "")),
|
|
39
|
+
"duration_seconds": str(context.get("duration_seconds", "")),
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
self._post(payload)
|
|
43
|
+
|
|
44
|
+
def _post(self, payload: Dict[str, Any]) -> None:
|
|
45
|
+
request = urllib.request.Request(
|
|
46
|
+
self.url,
|
|
47
|
+
data=json.dumps(payload, sort_keys=True).encode("utf-8"),
|
|
48
|
+
headers={"Content-Type": "application/json"},
|
|
49
|
+
method="POST",
|
|
50
|
+
)
|
|
51
|
+
try:
|
|
52
|
+
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
|
|
53
|
+
if int(response.status) < 200 or int(response.status) >= 300:
|
|
54
|
+
raise RuntimeError("audit sink rejected event")
|
|
55
|
+
except Exception:
|
|
56
|
+
self.failure_count += 1
|
|
57
|
+
if self.fail_closed:
|
|
58
|
+
raise
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class KafkaRestProgressEventSink:
|
|
63
|
+
url: str
|
|
64
|
+
topic: str
|
|
65
|
+
timeout_seconds: float = 2.0
|
|
66
|
+
fail_closed: bool = False
|
|
67
|
+
failure_count: int = 0
|
|
68
|
+
|
|
69
|
+
def __call__(self, event: Dict[str, Any]) -> None:
|
|
70
|
+
payload = {
|
|
71
|
+
"topic": self.topic,
|
|
72
|
+
"event": _redacted_progress_event(event),
|
|
73
|
+
}
|
|
74
|
+
request = urllib.request.Request(
|
|
75
|
+
self.url,
|
|
76
|
+
data=json.dumps(payload, sort_keys=True).encode("utf-8"),
|
|
77
|
+
headers={"Content-Type": "application/json"},
|
|
78
|
+
method="POST",
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
|
|
82
|
+
if int(response.status) < 200 or int(response.status) >= 300:
|
|
83
|
+
raise RuntimeError("audit sink rejected event")
|
|
84
|
+
except Exception:
|
|
85
|
+
self.failure_count += 1
|
|
86
|
+
if self.fail_closed:
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _redacted_progress_event(event: Dict[str, Any]) -> Dict[str, str]:
|
|
91
|
+
return {
|
|
92
|
+
field: str(event[field])
|
|
93
|
+
for field in _AUDIT_EVENT_FIELDS
|
|
94
|
+
if field in event and event[field] is not None
|
|
95
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from os import environ
|
|
9
|
+
from typing import Dict, Mapping, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ExternalBackendConfig:
|
|
14
|
+
redis_url: str = ""
|
|
15
|
+
milvus_url: str = ""
|
|
16
|
+
kafka_audit_url: str = ""
|
|
17
|
+
kafka_audit_topic: str = ""
|
|
18
|
+
timeout_seconds: float = 2.0
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_env(
|
|
22
|
+
cls,
|
|
23
|
+
env: Optional[Mapping[str, str]] = None,
|
|
24
|
+
) -> "ExternalBackendConfig":
|
|
25
|
+
source = env if env is not None else environ
|
|
26
|
+
return cls(
|
|
27
|
+
redis_url=source.get("KAGENT_REDIS_URL", cls.redis_url),
|
|
28
|
+
milvus_url=source.get("KAGENT_MILVUS_URL", cls.milvus_url),
|
|
29
|
+
kafka_audit_url=source.get(
|
|
30
|
+
"KAGENT_KAFKA_AUDIT_URL",
|
|
31
|
+
cls.kafka_audit_url,
|
|
32
|
+
),
|
|
33
|
+
kafka_audit_topic=source.get(
|
|
34
|
+
"KAGENT_KAFKA_AUDIT_TOPIC",
|
|
35
|
+
cls.kafka_audit_topic,
|
|
36
|
+
),
|
|
37
|
+
timeout_seconds=_env_float(
|
|
38
|
+
source,
|
|
39
|
+
"KAGENT_EXTERNAL_BACKEND_TIMEOUT_SECONDS",
|
|
40
|
+
cls.timeout_seconds,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
if self.timeout_seconds <= 0:
|
|
46
|
+
raise ValueError("external backend timeout_seconds must be positive")
|
|
47
|
+
|
|
48
|
+
def redacted_snapshot(self) -> Dict[str, str]:
|
|
49
|
+
return {
|
|
50
|
+
"redis_short_term_memory": "enabled" if self.redis_url else "disabled",
|
|
51
|
+
"milvus_long_term_memory": "enabled" if self.milvus_url else "disabled",
|
|
52
|
+
"kafka_audit_sink": "enabled" if self.kafka_audit_url else "disabled",
|
|
53
|
+
"kafka_audit_topic_configured": str(bool(self.kafka_audit_topic)).lower(),
|
|
54
|
+
"external_backend_timeout_seconds": str(self.timeout_seconds),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check_external_backends(config: ExternalBackendConfig) -> Dict[str, str]:
|
|
59
|
+
checks: Dict[str, str] = {}
|
|
60
|
+
if config.redis_url:
|
|
61
|
+
checks["redis_short_term_memory"] = _check(
|
|
62
|
+
lambda: _redis_ping(config.redis_url, timeout_seconds=config.timeout_seconds),
|
|
63
|
+
"redis_unavailable",
|
|
64
|
+
)
|
|
65
|
+
if config.milvus_url:
|
|
66
|
+
checks["milvus_long_term_memory"] = _check(
|
|
67
|
+
lambda: _http_get_ok(
|
|
68
|
+
config.milvus_url,
|
|
69
|
+
timeout_seconds=config.timeout_seconds,
|
|
70
|
+
),
|
|
71
|
+
"milvus_unavailable",
|
|
72
|
+
)
|
|
73
|
+
if config.kafka_audit_url:
|
|
74
|
+
checks["kafka_audit_sink"] = _check(
|
|
75
|
+
lambda: _http_get_ok(
|
|
76
|
+
config.kafka_audit_url,
|
|
77
|
+
timeout_seconds=config.timeout_seconds,
|
|
78
|
+
),
|
|
79
|
+
"kafka_audit_unavailable",
|
|
80
|
+
)
|
|
81
|
+
return checks
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _check(check, failure_code: str) -> str:
|
|
85
|
+
try:
|
|
86
|
+
check()
|
|
87
|
+
except Exception:
|
|
88
|
+
return f"failed: {failure_code}"
|
|
89
|
+
return "ok"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _redis_ping(redis_url: str, *, timeout_seconds: float) -> None:
|
|
93
|
+
parsed = urllib.parse.urlparse(redis_url)
|
|
94
|
+
if parsed.scheme != "redis":
|
|
95
|
+
raise ValueError("redis url must use redis://")
|
|
96
|
+
if not parsed.hostname:
|
|
97
|
+
raise ValueError("redis url host is required")
|
|
98
|
+
port = parsed.port or 6379
|
|
99
|
+
with socket.create_connection(
|
|
100
|
+
(parsed.hostname, port),
|
|
101
|
+
timeout=timeout_seconds,
|
|
102
|
+
) as conn:
|
|
103
|
+
conn.settimeout(timeout_seconds)
|
|
104
|
+
conn.sendall(b"*1\r\n$4\r\nPING\r\n")
|
|
105
|
+
response = conn.recv(64)
|
|
106
|
+
if not response.startswith(b"+PONG"):
|
|
107
|
+
raise RuntimeError("redis ping failed")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _http_get_ok(url: str, *, timeout_seconds: float) -> None:
|
|
111
|
+
parsed = urllib.parse.urlparse(url)
|
|
112
|
+
if parsed.scheme not in {"http", "https"}:
|
|
113
|
+
raise ValueError("backend url must use http:// or https://")
|
|
114
|
+
request = urllib.request.Request(url, method="GET")
|
|
115
|
+
try:
|
|
116
|
+
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
|
|
117
|
+
status_code = int(response.status)
|
|
118
|
+
except urllib.error.HTTPError as exc:
|
|
119
|
+
status_code = int(exc.code)
|
|
120
|
+
if status_code < 200 or status_code >= 300:
|
|
121
|
+
raise RuntimeError("backend health check failed")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _env_float(source: Mapping[str, str], name: str, default: float) -> float:
|
|
125
|
+
value = source.get(name, "")
|
|
126
|
+
if value == "":
|
|
127
|
+
return default
|
|
128
|
+
try:
|
|
129
|
+
return float(value)
|
|
130
|
+
except ValueError as exc:
|
|
131
|
+
raise ValueError(f"{name} must be a float") from exc
|