botnesia-core 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.
Files changed (70) hide show
  1. agent_observability.py +402 -0
  2. agent_os.py +180 -0
  3. agent_registry.py +170 -0
  4. ai_providers/__init__.py +20 -0
  5. ai_providers/base.py +30 -0
  6. ai_providers/deepseek.py +206 -0
  7. ai_providers/gemini.py +441 -0
  8. ai_providers/groq_provider.py +176 -0
  9. ai_providers/openrouter.py +254 -0
  10. ai_providers/router.py +371 -0
  11. ai_providers/types.py +73 -0
  12. anti_hallucination_engine.py +119 -0
  13. base.py +631 -0
  14. botnesia_core-0.1.0.dist-info/METADATA +253 -0
  15. botnesia_core-0.1.0.dist-info/RECORD +70 -0
  16. botnesia_core-0.1.0.dist-info/WHEEL +5 -0
  17. botnesia_core-0.1.0.dist-info/licenses/LICENSE +202 -0
  18. botnesia_core-0.1.0.dist-info/licenses/NOTICE +5 -0
  19. botnesia_core-0.1.0.dist-info/top_level.txt +34 -0
  20. cognitive_loop/__init__.py +16 -0
  21. cognitive_loop/loop.py +151 -0
  22. cognitive_loop/worker.py +32 -0
  23. cost_intelligence.py +196 -0
  24. devil_advocate_agent.py +116 -0
  25. evaluation/__init__.py +15 -0
  26. evaluation/evaluator.py +115 -0
  27. evaluation/schema.py +30 -0
  28. event_bus/__init__.py +46 -0
  29. event_bus/bus.py +101 -0
  30. event_bus/events.py +26 -0
  31. feature_flags/__init__.py +27 -0
  32. feature_flags/flags.py +101 -0
  33. first_principle_agent.py +139 -0
  34. groq_knowledge.py +345 -0
  35. intent_classifier.py +84 -0
  36. kb_embeddings.py +98 -0
  37. knowledge_access_engine.py +209 -0
  38. long_term_memory/__init__.py +19 -0
  39. long_term_memory/schema.py +57 -0
  40. long_term_memory/store.py +126 -0
  41. mcp_client.py +147 -0
  42. mcp_registry.py +148 -0
  43. multi_agent_orchestrator.py +437 -0
  44. perf_cache.py +76 -0
  45. planner_agent.py +81 -0
  46. platform_state/__init__.py +22 -0
  47. platform_state/base.py +73 -0
  48. platform_state/factory.py +26 -0
  49. platform_state/inprocess.py +140 -0
  50. platform_state/redis_store.py +124 -0
  51. policy_engine/__init__.py +21 -0
  52. policy_engine/engine.py +98 -0
  53. policy_engine/loader.py +76 -0
  54. prompt_registry/__init__.py +22 -0
  55. prompt_registry/factory.py +19 -0
  56. prompt_registry/registry.py +160 -0
  57. prompt_registry/schema.py +37 -0
  58. socratic_reasoning.py +119 -0
  59. task_engine.py +143 -0
  60. task_runtime/__init__.py +22 -0
  61. task_runtime/monitor.py +142 -0
  62. task_runtime/repository.py +230 -0
  63. task_runtime/runner.py +344 -0
  64. task_runtime/schema.py +64 -0
  65. task_runtime/worker.py +65 -0
  66. tool_executor.py +836 -0
  67. tool_registry.py +308 -0
  68. uncertainty_engine.py +195 -0
  69. vendor_bootstrap.py +28 -0
  70. workflow_engine.py +646 -0
agent_observability.py ADDED
@@ -0,0 +1,402 @@
1
+ """Persistent, fail-open tracing for the BotNesia multi-agent pipeline."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import contextvars
6
+ import json
7
+ import os
8
+ import time
9
+ import traceback
10
+ import uuid
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timezone
13
+ from decimal import Decimal
14
+ from typing import Any, Awaitable, Callable, TypeVar
15
+
16
+ import httpx
17
+
18
+ # Optional-dependency seam (Open-Core extraction, blocker R1):
19
+ # In the Cloud/Enterprise deployment `bn_platform` is always present and these are
20
+ # the real Prometheus recorders — behaviour is byte-identical. When this module is
21
+ # reused inside the open `botnesia-core` package (where `bn_platform` is absent),
22
+ # the import degrades to fail-open no-ops instead of raising ImportError. This keeps
23
+ # the core importable without pulling in any business/cloud code. No public API
24
+ # changes: `add_token_usage`/`observe_agent` (imported by base.py) are unaffected.
25
+ try:
26
+ from bn_platform.observability import record_ai_request, record_token_usage
27
+ except ImportError: # pragma: no cover - core-only path (bn_platform not installed)
28
+ def record_ai_request(*args, **kwargs) -> None: # type: ignore[misc]
29
+ return None
30
+
31
+ def record_token_usage(*args, **kwargs) -> None: # type: ignore[misc]
32
+ return None
33
+
34
+ from cost_intelligence import choose_model, estimate_cost_usd, reset_model_route, set_model_route
35
+
36
+ T = TypeVar("T")
37
+
38
+ # ── Auto-retry untuk kegagalan TRANSIENT (timeout/jaringan/API sementara) ──
39
+ # Provider LLM sudah retry di level HTTP; ini jaring pengaman level-agent untuk
40
+ # error transient yang lolos dari provider. Konfigurasi via env.
41
+ _RETRY_MAX = int(os.getenv("AGENT_RETRY_MAX", "3"))
42
+ _RETRY_BASE_DELAY = float(os.getenv("AGENT_RETRY_BASE_DELAY", "0.5"))
43
+ _RETRY_MAX_DELAY = float(os.getenv("AGENT_RETRY_MAX_DELAY", "8"))
44
+ _TRANSIENT_TYPES = (
45
+ httpx.TimeoutException, httpx.ConnectError, httpx.NetworkError,
46
+ httpx.RemoteProtocolError, httpx.PoolTimeout, asyncio.TimeoutError,
47
+ ConnectionError,
48
+ )
49
+ _TRANSIENT_KEYWORDS = (
50
+ "timed out", "timeout", "temporarily unavailable", "service unavailable",
51
+ "connection reset", "connection aborted", "rate limit", "too many requests",
52
+ "502", "503", "504", "bad gateway", "gateway timeout",
53
+ )
54
+
55
+
56
+ # ── Realtime event publisher (di-wire main ke ObservabilityHub) ──
57
+ # None = tidak ada dashboard realtime (fail-open). Dipanggil non-blocking.
58
+ _event_publisher: Callable[[str, dict], Any] | None = None
59
+
60
+
61
+ def set_event_publisher(fn: Callable[[str, dict], Any] | None) -> None:
62
+ global _event_publisher
63
+ _event_publisher = fn
64
+
65
+
66
+ def _emit(org_id: str, event: dict) -> None:
67
+ """Publish event realtime tanpa memblokir jalur eksekusi (fire-and-forget)."""
68
+ if not _event_publisher or not org_id:
69
+ return
70
+ try:
71
+ coro = _event_publisher(org_id, event)
72
+ if asyncio.iscoroutine(coro):
73
+ task = asyncio.ensure_future(coro)
74
+ task.add_done_callback(lambda t: t.exception()) # jangan bocorkan exc
75
+ except Exception:
76
+ pass
77
+
78
+
79
+ def _is_transient(exc: BaseException) -> bool:
80
+ """True bila error layak di-retry (transient), bukan bug logika/permanen."""
81
+ if isinstance(exc, _TRANSIENT_TYPES):
82
+ return True
83
+ resp = getattr(exc, "response", None)
84
+ if resp is not None and getattr(resp, "status_code", None) in (429, 500, 502, 503, 504):
85
+ return True
86
+ msg = str(exc).lower()
87
+ return any(k in msg for k in _TRANSIENT_KEYWORDS)
88
+
89
+
90
+ @dataclass
91
+ class ModelUsage:
92
+ model: str
93
+ prompt_tokens: int
94
+ completion_tokens: int
95
+ estimated_cost: Decimal
96
+
97
+
98
+ @dataclass
99
+ class TokenUsage:
100
+ prompt_tokens: int = 0
101
+ completion_tokens: int = 0
102
+ model_usages: list[ModelUsage] = field(default_factory=list)
103
+
104
+ @property
105
+ def total_tokens(self) -> int:
106
+ return self.prompt_tokens + self.completion_tokens
107
+
108
+
109
+ @dataclass
110
+ class TraceState:
111
+ trace_id: str
112
+ tenant_id: str
113
+ conversation_id: str
114
+ channel: str = "widget"
115
+ actual_model: str = ""
116
+ pool: Any = None
117
+ sequence: int = 0
118
+ request_tokens: TokenUsage = field(default_factory=TokenUsage)
119
+
120
+ def next_sequence(self) -> int:
121
+ self.sequence += 1
122
+ return self.sequence
123
+
124
+
125
+ _trace_state: contextvars.ContextVar[TraceState | None] = contextvars.ContextVar("botnesia_trace_state", default=None)
126
+ _execution_id: contextvars.ContextVar[str | None] = contextvars.ContextVar("botnesia_execution_id", default=None)
127
+ _execution_tokens: contextvars.ContextVar[TokenUsage | None] = contextvars.ContextVar("botnesia_execution_tokens", default=None)
128
+
129
+
130
+ def add_token_usage(*, model: str, prompt_tokens: int = 0, completion_tokens: int = 0) -> None:
131
+ """Attach provider token usage and estimated cost to the active execution."""
132
+ prompt = max(0, int(prompt_tokens or 0))
133
+ completion = max(0, int(completion_tokens or 0))
134
+ model_name = model or "unknown"
135
+ state = _trace_state.get()
136
+ current = _execution_tokens.get()
137
+ if current is not None:
138
+ current.prompt_tokens += prompt
139
+ current.completion_tokens += completion
140
+ current.model_usages.append(
141
+ ModelUsage(
142
+ model=model_name,
143
+ prompt_tokens=prompt,
144
+ completion_tokens=completion,
145
+ estimated_cost=estimate_cost_usd(model_name, prompt, completion),
146
+ )
147
+ )
148
+ if state is not None:
149
+ state.request_tokens.prompt_tokens += prompt
150
+ state.request_tokens.completion_tokens += completion
151
+ state.actual_model = model_name
152
+ record_token_usage(
153
+ org_id=state.tenant_id if state else None,
154
+ model=model_name,
155
+ prompt_tokens=prompt,
156
+ completion_tokens=completion,
157
+ )
158
+
159
+
160
+ def _confidence(value: Any) -> float | None:
161
+ output = getattr(value, "output", None)
162
+ if isinstance(output, dict):
163
+ raw = output.get("confidence_score", output.get("confidence"))
164
+ elif isinstance(value, dict):
165
+ raw = value.get("confidence_score", value.get("confidence"))
166
+ else:
167
+ raw = getattr(value, "confidence_score", None)
168
+ return float(raw) if isinstance(raw, (int, float)) else None
169
+
170
+
171
+ def _status(value: Any) -> tuple[str, str | None]:
172
+ success = getattr(value, "success", True)
173
+ error = getattr(value, "error", None)
174
+ output = getattr(value, "output", value if isinstance(value, dict) else None)
175
+ if success is False:
176
+ return "error", str(error or "Agent execution failed")
177
+ if isinstance(output, dict) and any(output.get(k) for k in
178
+ ("waiting", "needs_approval", "awaiting_approval", "pending_approval")):
179
+ return "waiting", None # menunggu approval/dependency (valid, bukan gagal)
180
+ if isinstance(output, dict) and output.get("skipped"):
181
+ return "skipped", None
182
+ return "success", None
183
+
184
+
185
+ def _output_summary(value: Any) -> dict:
186
+ output = getattr(value, "output", value if isinstance(value, dict) else {})
187
+ if not isinstance(output, dict):
188
+ return {}
189
+ allowed = (
190
+ "reasoning_summary", "conclusion", "limitations", "suggested_next_action",
191
+ "verified", "issues", "complexity", "source", "matched", "has_objection",
192
+ "should_escalate", "urgency", "intent", "skipped", "reason",
193
+ "risk_if_wrong", "needs_clarification", "severity",
194
+ "needs_revision", "overstatement_risk", "revised",
195
+ "causal_links_count", "root_hypotheses_count",
196
+ "uncertainty_band", "uncertainty_score", "uncertainty_reasons",
197
+ )
198
+ return {key: output[key] for key in allowed if key in output}
199
+
200
+
201
+ async def _execute(pool: Any, sql: str, *args: Any) -> None:
202
+ if pool is None:
203
+ return
204
+ try:
205
+ await pool.execute(sql, *args)
206
+ except Exception:
207
+ # Observability and cost accounting must never break customer responses.
208
+ return
209
+
210
+
211
+ async def observe_agent(agent_name: str, context: dict, operation: Callable[[], Awaitable[T]]) -> T:
212
+ """Track one agent lifecycle, token usage, and provider cost."""
213
+ state = _trace_state.get()
214
+ if state is None:
215
+ return await operation()
216
+
217
+ execution_id = str(uuid.uuid4())
218
+ parent_id = _execution_id.get()
219
+ sequence = state.next_sequence()
220
+ started_at = datetime.now(timezone.utc)
221
+ started_perf = time.perf_counter()
222
+ usage = TokenUsage()
223
+ id_token = _execution_id.set(execution_id)
224
+ usage_token = _execution_tokens.set(usage)
225
+ await _execute(
226
+ state.pool,
227
+ """INSERT INTO agent_executions
228
+ (id, trace_id, parent_execution_id, tenant_id, conversation_id,
229
+ agent_name, sequence_no, execution_start, status, created_at)
230
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'running',NOW())""",
231
+ execution_id, state.trace_id, parent_id, state.tenant_id,
232
+ state.conversation_id, agent_name, sequence, started_at,
233
+ )
234
+ _emit(state.tenant_id, {"type": "agent", "agent_name": agent_name,
235
+ "status": "running", "trace_id": state.trace_id,
236
+ "ts": started_at.isoformat()})
237
+
238
+ result: Any = None
239
+ retry_count = 0
240
+ error_stack: str | None = None
241
+ # Nilai default: bila alur keluar lewat BaseException tak terduga, finally tetap
242
+ # punya status/error/confidence yang terikat (cegah UnboundLocalError).
243
+ status, error, confidence = "running", None, None
244
+ try:
245
+ # Auto-retry TRANSIENT dengan exponential backoff (maks _RETRY_MAX).
246
+ # Error non-transient (bug logika, permission, value) gagal cepat.
247
+ while True:
248
+ try:
249
+ result = await operation()
250
+ break
251
+ except Exception as exc: # noqa: BLE001 — klasifikasi transient di bawah
252
+ # Cegah multiplikasi retry pada observe_agent BERSARANG (mis.
253
+ # supervisor membungkus operation yang juga memanggil observe_agent):
254
+ # exception transient yang sudah di-retry di level dalam ditandai,
255
+ # sehingga wrapper luar tidak me-retry ulang.
256
+ already_retried = getattr(exc, "_bn_retried", False)
257
+ if already_retried or retry_count >= _RETRY_MAX or not _is_transient(exc):
258
+ if _is_transient(exc) and retry_count and not already_retried:
259
+ try:
260
+ exc._bn_retried = True
261
+ except Exception:
262
+ pass
263
+ raise
264
+ retry_count += 1
265
+ await _execute(
266
+ state.pool,
267
+ "UPDATE agent_executions SET status='retrying', retry_count=$2 WHERE id=$1",
268
+ execution_id, retry_count,
269
+ )
270
+ _emit(state.tenant_id, {"type": "agent", "agent_name": agent_name,
271
+ "status": "retrying", "retry_count": retry_count,
272
+ "trace_id": state.trace_id})
273
+ await asyncio.sleep(min(_RETRY_BASE_DELAY * (2 ** (retry_count - 1)), _RETRY_MAX_DELAY))
274
+ status, error = _status(result)
275
+ confidence = _confidence(result)
276
+ return result
277
+ except asyncio.CancelledError:
278
+ # Request dibatalkan/timeout klien → CANCELLED (BaseException, TIDAK
279
+ # ditangkap `except Exception`). Bukan bug/kegagalan; harus di-reraise
280
+ # agar pembatalan task berjalan benar.
281
+ status, error, confidence = "cancelled", "Dibatalkan (request cancelled/timeout klien)", None
282
+ raise
283
+ except Exception as exc:
284
+ # str(exc) bisa KOSONG untuk sejumlah exception (mis. CancelledError,
285
+ # TimeoutError tanpa pesan) → dulu error_message tersimpan blank sehingga
286
+ # dashboard menampilkan FAILED tanpa alasan. Selalu sertakan tipe exception
287
+ # agar setiap kegagalan punya root cause yang bisa dibaca.
288
+ _detail = str(exc).strip()
289
+ error = f"{type(exc).__name__}: {_detail}" if _detail else type(exc).__name__
290
+ if retry_count:
291
+ error = f"{error} (setelah {retry_count} retry)"
292
+ # Stacktrace lengkap untuk panel error-detail (dipangkas agar tak membengkak).
293
+ error_stack = traceback.format_exc()[-6000:]
294
+ status, confidence = "error", None
295
+ raise
296
+ finally:
297
+ duration_ms = int((time.perf_counter() - started_perf) * 1000)
298
+ await _execute(
299
+ state.pool,
300
+ """UPDATE agent_executions
301
+ SET execution_end=$2, duration_ms=$3, status=$4, error_message=$5,
302
+ confidence_score=$6, prompt_tokens=$7, completion_tokens=$8,
303
+ total_tokens=$9, metadata=$10::jsonb, retry_count=$11, error_stack=$12
304
+ WHERE id=$1""",
305
+ execution_id, datetime.now(timezone.utc), duration_ms, status, error, confidence,
306
+ usage.prompt_tokens, usage.completion_tokens, usage.total_tokens,
307
+ json.dumps(_output_summary(result), ensure_ascii=True), retry_count, error_stack,
308
+ )
309
+ _emit(state.tenant_id, {"type": "agent", "agent_name": agent_name,
310
+ "status": status, "retry_count": retry_count,
311
+ "error_message": error, "duration_ms": duration_ms,
312
+ "trace_id": state.trace_id})
313
+ for model_usage in usage.model_usages:
314
+ await _execute(
315
+ state.pool,
316
+ """INSERT INTO cost_records
317
+ (id, tenant_id, conversation_id, trace_id, execution_id,
318
+ model_name, agent_name, prompt_tokens, completion_tokens,
319
+ token_count, estimated_cost, currency, channel, created_at)
320
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'USD',$12,NOW())""",
321
+ str(uuid.uuid4()), state.tenant_id, state.conversation_id,
322
+ state.trace_id, execution_id, model_usage.model, agent_name,
323
+ model_usage.prompt_tokens, model_usage.completion_tokens,
324
+ model_usage.prompt_tokens + model_usage.completion_tokens,
325
+ model_usage.estimated_cost, state.channel,
326
+ )
327
+ record_ai_request(
328
+ agent=agent_name,
329
+ success=status in {"success", "skipped"},
330
+ duration_seconds=duration_ms / 1000,
331
+ )
332
+ _execution_tokens.reset(usage_token)
333
+ _execution_id.reset(id_token)
334
+
335
+
336
+ async def trace_request(context: dict, operation: Callable[[], Awaitable[T]]) -> T:
337
+ """Create one request trace and select the cost-efficient model tier."""
338
+ tenant_id = str(context.get("org_id") or context.get("tenant_id") or "")
339
+ conversation_id = str(context.get("conversation_id") or "")
340
+ pool = context.get("_observability_pool")
341
+ if not tenant_id or not conversation_id:
342
+ return await operation()
343
+
344
+ trace_id = str(uuid.uuid4())
345
+ metadata = context.get("metadata") or {}
346
+ channel = str(metadata.get("channel") or context.get("channel") or "widget")
347
+ route = choose_model(
348
+ str(context.get("user_message") or ""),
349
+ str(context.get("reasoning_mode") or "standard"),
350
+ str(context.get("_cheap_model") or "llama-3.1-8b-instant"),
351
+ str(context.get("_strong_model") or "meta-llama/llama-4-scout-17b-16e-instruct"),
352
+ )
353
+ route_token = set_model_route(route)
354
+ state = TraceState(
355
+ trace_id=trace_id, tenant_id=tenant_id, conversation_id=conversation_id,
356
+ channel=channel, actual_model=route.model, pool=pool,
357
+ )
358
+ state_token = _trace_state.set(state)
359
+ started = datetime.now(timezone.utc)
360
+ await _execute(
361
+ pool,
362
+ """INSERT INTO ai_traces
363
+ (id, tenant_id, conversation_id, user_question, status, started_at,
364
+ routed_model, task_complexity, channel, created_at)
365
+ VALUES ($1,$2,$3,$4,'running',$5,$6,$7,$8,NOW())""",
366
+ trace_id, tenant_id, conversation_id,
367
+ str(context.get("user_message") or "")[:10000], started,
368
+ route.model, route.complexity, channel,
369
+ )
370
+
371
+ final_answer = ""
372
+ trace_status = "error"
373
+ try:
374
+ result = await observe_agent("supervisor_agent", context, operation)
375
+ final_answer = str(getattr(result, "final_answer", "") or "")
376
+ try:
377
+ result.prompt_tokens = state.request_tokens.prompt_tokens
378
+ result.completion_tokens = state.request_tokens.completion_tokens
379
+ result.total_tokens = state.request_tokens.total_tokens
380
+ result.routed_model = state.actual_model
381
+ result.task_complexity = route.complexity
382
+ except (AttributeError, TypeError):
383
+ pass
384
+ trace_status = "error" if getattr(result, "errors", []) and not final_answer else "success"
385
+ return result
386
+ finally:
387
+ duration_ms = int((datetime.now(timezone.utc) - started).total_seconds() * 1000)
388
+ await _execute(
389
+ pool,
390
+ """UPDATE ai_traces
391
+ SET final_answer=$2, status=$3, ended_at=NOW(), duration_ms=$4,
392
+ prompt_tokens=$5, completion_tokens=$6, total_tokens=$7,
393
+ routed_model=$8
394
+ WHERE id=$1""",
395
+ trace_id, final_answer[:20000], trace_status, duration_ms,
396
+ state.request_tokens.prompt_tokens,
397
+ state.request_tokens.completion_tokens,
398
+ state.request_tokens.total_tokens,
399
+ state.actual_model,
400
+ )
401
+ _trace_state.reset(state_token)
402
+ reset_model_route(route_token)
agent_os.py ADDED
@@ -0,0 +1,180 @@
1
+ """
2
+ agent_os.py — Agent OS Layer (transparansi pipeline) untuk BotNesia.
3
+
4
+ Modul ini TIDAK memanggil LLM, TIDAK mengubah state apapun, dan TIDAK
5
+ dipanggil dari `supervisor.py`'s `_process()`. Ini murni lapisan PELAPORAN
6
+ di atas pipeline yang SUDAH ADA -- `SupervisorAgent._process()` sudah
7
+ menjalankan plan -> tool-select -> execute -> verify -> retry -> report
8
+ secara penuh lewat modul-modul yang sudah berdiri sendiri (planner_agent,
9
+ reasoning_controller, cs_agent, verification_agent, reflection_engine,
10
+ uncertainty_engine). `build_execution_report()` di sini cuma membaca ulang
11
+ field yang sudah ada di `SupervisorResult` dan menyusunnya ke bentuk
12
+ "Agent OS 6-stage" supaya bisa ditampilkan (dashboard/identity) tanpa
13
+ membongkar pipeline aslinya.
14
+
15
+ Dipanggil oleh consumer DI LUAR pipeline (mis. endpoint dashboard di fase
16
+ berikutnya) -- bukan oleh `supervisor.py` sendiri, supaya modul ini tetap
17
+ nol-coupling terhadap jalur chat yang sudah stabil.
18
+
19
+ Sejak AI Agent Platform Phase: menambahkan service catalog untuk semua
20
+ service baru (ComputerUseService, FileSystemService, TerminalService, dll)
21
+ via `describe_agent_platform()` — murni dokumentasi, tidak ada eksekusi.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ from typing import Any
26
+
27
+ AGENT_OS_STAGES: dict[str, dict] = {
28
+ "planning": {
29
+ "description": "Menyusun rencana analisis (lensa spesialis mana yang relevan) untuk pertanyaan kompleks.",
30
+ "implementation": "planner_agent.PlannerAgent",
31
+ },
32
+ "tool_selection": {
33
+ "description": "Menentukan sumber pengetahuan/tool mana yang relevan (memory, knowledge base, web search, dst) beserta alasannya.",
34
+ "implementation": "reasoning_controller.ReasoningController.analyze (knowledge_routing)",
35
+ },
36
+ "execution": {
37
+ "description": "Menjalankan lensa spesialis (Pro mode) dan menyusun jawaban akhir.",
38
+ "implementation": "reasoning_agent.ReasoningAgent + cs_agent.CSAgent.synthesize",
39
+ },
40
+ "verification": {
41
+ "description": "Memeriksa kualitas jawaban: risiko halusinasi, kelengkapan, konsistensi.",
42
+ "implementation": "verification_agent.VerificationAgent",
43
+ },
44
+ "retry": {
45
+ "description": "Mengulang sintesis (maksimal beberapa kali) bila verifikasi belum lolos confidence minimum.",
46
+ "implementation": "supervisor.SupervisorAgent._process MAX_RETRIES loop",
47
+ },
48
+ "reporting": {
49
+ "description": "Self-check akhir terhadap reasoning brief, lalu menetapkan band confidence (High/Medium/Low) untuk dilaporkan ke user.",
50
+ "implementation": "reflection_engine.reflect + uncertainty_engine.UncertaintyEngine",
51
+ },
52
+ }
53
+
54
+
55
+ def describe_stage(name: str) -> dict:
56
+ """Lookup-with-default, mirror tool_registry.describe_tool()."""
57
+ return AGENT_OS_STAGES.get(name, {"description": "Stage tidak dikenal.", "implementation": "-"})
58
+
59
+
60
+ AGENT_PLATFORM_SERVICES: dict[str, dict] = {
61
+ "ComputerUseService": {
62
+ "module": "computer_use_service",
63
+ "description": "Browser automation + native app interaction dengan permission gate",
64
+ "capabilities": ["navigate", "read_text", "screenshot", "scroll", "click", "fill", "submit",
65
+ "inspect_desktop", "move_mouse", "double_click", "right_click", "drag",
66
+ "type_text", "press_hotkey", "open_application", "login", "scrape_page"],
67
+ "requires_permission": ["browser_access", "browser_write", "screen"],
68
+ },
69
+ "FileSystemService": {
70
+ "module": "file_system_service",
71
+ "description": "Operasi file dengan permission gate (read/write/edit/rename/move/delete/compress/extract)",
72
+ "capabilities": ["read_file", "write_file", "edit_file", "rename_file", "move_file",
73
+ "copy_file", "delete_file", "list_directory", "search_files",
74
+ "compress", "extract", "understand_project"],
75
+ "requires_permission": ["read_files", "write_files", "delete_files"],
76
+ },
77
+ "TerminalService": {
78
+ "module": "terminal_service",
79
+ "description": "Shell command execution dengan timeout, approval gate, dan audit",
80
+ "capabilities": ["execute", "git", "run_python", "npm", "pnpm", "docker",
81
+ "read_log", "kill_process", "list_processes"],
82
+ "requires_permission": ["run_terminal"],
83
+ },
84
+ "PermissionManager": {
85
+ "module": "permission_manager",
86
+ "description": "Enterprise permission model: Allow Once/Always/Deny per resource per org",
87
+ "capabilities": ["check", "grant", "revoke", "list_grants"],
88
+ "permissions": ["read_files", "write_files", "delete_files", "run_terminal",
89
+ "browser_access", "browser_write", "github_access", "database_access",
90
+ "email_access", "api_access", "clipboard", "camera", "microphone", "screen"],
91
+ },
92
+ "SandboxManager": {
93
+ "module": "sandbox_manager",
94
+ "description": "Isolasi eksekusi: temporary workspace, rollback, resource limits",
95
+ "capabilities": ["create_session", "rollback", "cleanup", "session (context manager)"],
96
+ },
97
+ "ActionExecutor": {
98
+ "module": "action_executor",
99
+ "description": "Pipeline: Understand→Plan→Permission→Execute→Observe→Recover→Verify→Summarize",
100
+ "capabilities": ["execute", "understand_goal", "plan_goal", "verify_goal", "summarize_execution"],
101
+ },
102
+ "RecoveryManager": {
103
+ "module": "recovery_manager",
104
+ "description": "Auto-retry dengan backoff eksponensial + circuit breaker + fallback",
105
+ "capabilities": ["with_retry", "with_fallback", "reset_circuit", "get_circuit_status"],
106
+ },
107
+ "AgentMemoryStore": {
108
+ "module": "agent_memory_store",
109
+ "description": "Agent-level memory: project structure, file history, terminal history, browser history",
110
+ "capabilities": ["record_file_opened", "record_command", "record_url_visited",
111
+ "record_action", "update_project_structure", "get_summary",
112
+ "save_to_db", "load_from_db"],
113
+ },
114
+ "AuditLogger": {
115
+ "module": "audit_logger",
116
+ "description": "Audit trail semua aksi agent ke tabel agent_audit_log",
117
+ "capabilities": ["log_action", "update_log", "list_logs"],
118
+ },
119
+ }
120
+
121
+
122
+ def describe_agent_platform_service(name: str) -> dict:
123
+ """Lookup service catalog AI Agent Platform."""
124
+ return AGENT_PLATFORM_SERVICES.get(name, {"description": "Service tidak dikenal.", "module": "-"})
125
+
126
+
127
+ def list_agent_platform_services() -> list[str]:
128
+ """Daftar semua service AI Agent Platform."""
129
+ return list(AGENT_PLATFORM_SERVICES.keys())
130
+
131
+
132
+ def build_execution_report(result: Any) -> dict:
133
+ """Reshape SupervisorResult yang sudah ada menjadi laporan 6-stage Agent OS.
134
+
135
+ `result` adalah instance supervisor.SupervisorResult (diketik Any di sini
136
+ supaya modul ini tidak perlu import supervisor.py -- hindari circular
137
+ import, karena supervisor.py-lah yang nanti bisa memanggil modul ini,
138
+ bukan sebaliknya). Fungsi ini murni, tidak mengubah `result` sama sekali.
139
+ """
140
+ reasoning_brief = getattr(result, "reasoning_brief", None) or {}
141
+ knowledge_routing = reasoning_brief.get("knowledge_routing") if isinstance(reasoning_brief, dict) else None
142
+
143
+ return {
144
+ "planning": {
145
+ **describe_stage("planning"),
146
+ "data": getattr(result, "plan", None),
147
+ },
148
+ "tool_selection": {
149
+ **describe_stage("tool_selection"),
150
+ "data": knowledge_routing,
151
+ },
152
+ "execution": {
153
+ **describe_stage("execution"),
154
+ "data": {
155
+ "reasoning_mode_used": getattr(result, "reasoning_mode_used", None),
156
+ "specialist_results": getattr(result, "specialist_results", None),
157
+ },
158
+ },
159
+ "verification": {
160
+ **describe_stage("verification"),
161
+ "data": {
162
+ "verification_passed": getattr(result, "verification_passed", None),
163
+ "verification_issues": getattr(result, "verification_issues", None),
164
+ "confidence_score": getattr(result, "confidence_score", None),
165
+ },
166
+ },
167
+ "retry": {
168
+ **describe_stage("retry"),
169
+ "data": {"retry_count": getattr(result, "retry_count", None)},
170
+ },
171
+ "reporting": {
172
+ **describe_stage("reporting"),
173
+ "data": {
174
+ "reflection_review": getattr(result, "reflection_review", None),
175
+ "uncertainty_band": getattr(result, "uncertainty_band", None),
176
+ "uncertainty_score": getattr(result, "uncertainty_score", None),
177
+ "uncertainty_reasons": getattr(result, "uncertainty_reasons", None),
178
+ },
179
+ },
180
+ }