oblivion-agent 2.9.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.
- agent/__init__.py +0 -0
- agent/brain.py +413 -0
- agent/code_chunker.py +431 -0
- agent/core.py +709 -0
- agent/core.py.backup +243 -0
- agent/core.py.bak-complex +513 -0
- agent/core.py.bak-prefix +423 -0
- agent/core.py.bak-workspace +467 -0
- agent/core.py.before_knowledge_patch +605 -0
- agent/friday.py +455 -0
- agent/llm.py +265 -0
- agent/models.py +156 -0
- agent/parser.py +218 -0
- agent/parser.py.bak-prefix +176 -0
- agent/paths.py +235 -0
- agent/rag.py +477 -0
- agent/runtime.py +467 -0
- agent/runtime.py.bak-complex +299 -0
- agent/runtime.py.bak-prefix +280 -0
- agent/runtime.py.before_knowledge_patch +384 -0
- agent/setup_wizard.py +234 -0
- agent/symbol_index.py +314 -0
- agent/voice.py +343 -0
- agent/watcher.py +176 -0
- db/__init__.py +0 -0
- db/store.py +188 -0
- knowledge/__init__.py +0 -0
- knowledge/detector.py +162 -0
- knowledge/injector.py +175 -0
- knowledge/packs/database.md +630 -0
- knowledge/packs/debugging.md +96 -0
- knowledge/packs/deployment.md +72 -0
- knowledge/packs/django.md +285 -0
- knowledge/packs/docker.md +699 -0
- knowledge/packs/fastapi.md +534 -0
- knowledge/packs/nextjs.md +129 -0
- knowledge/packs/react.md +142 -0
- knowledge/packs/security.md +882 -0
- knowledge/packs/tailwind.md +107 -0
- knowledge/packs/testing.md +718 -0
- knowledge/packs/typescript.md +599 -0
- knowledge/packs/vue.md +123 -0
- mcp_server/__init__.py +0 -0
- mcp_server/server.py +160 -0
- oblivion_agent-2.9.0.dist-info/METADATA +140 -0
- oblivion_agent-2.9.0.dist-info/RECORD +74 -0
- oblivion_agent-2.9.0.dist-info/WHEEL +4 -0
- oblivion_agent-2.9.0.dist-info/entry_points.txt +2 -0
- oblivion_agent-2.9.0.dist-info/licenses/LICENSE +21 -0
- tools/__init__.py +0 -0
- tools/auto.py +240 -0
- tools/bash.py +169 -0
- tools/bash.py.bak-complex +29 -0
- tools/diff.py +75 -0
- tools/edit_file.py +77 -0
- tools/filesystem.py +270 -0
- tools/planner.py +53 -0
- tools/registry.py +291 -0
- tools/registry.py.bak-complex +250 -0
- tools/registry.py.bak-workspace +234 -0
- tools/search_code.py +127 -0
- tools/symbol_tools.py +212 -0
- ui/__init__.py +0 -0
- ui/app.py +2332 -0
- ui/app.py.backup +1634 -0
- ui/app.py.backup.20260612_181912 +1886 -0
- ui/app.py.bak-complex +1981 -0
- ui/app.py.bak-workspace +1907 -0
- ui/app.py.before_dedupe.20260612_110555 +1910 -0
- ui/app.py.before_dedupe.20260612_110629 +1797 -0
- ui/app.py.before_dockfix.20260612_112427 +1797 -0
- ui/app.py.before_wavefix.20260612_112136 +1797 -0
- ui/app.py.cyberpunk_backup +2011 -0
- ui/app.py.v2_backup +2032 -0
agent/__init__.py
ADDED
|
File without changes
|
agent/brain.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brain upgrade: Planning, Verification, and Memory.
|
|
3
|
+
|
|
4
|
+
Three new capabilities:
|
|
5
|
+
1. PLAN - structured execution plans for complex tasks
|
|
6
|
+
2. VERIFY - syntax/test checks after code writes
|
|
7
|
+
3. MEMORY - persistent workspace knowledge (MEMORY.md)
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from dataclasses import dataclass, field, asdict
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
# ── MEMORY ────────────────────────────────────────────────────────────────────
|
|
17
|
+
def get_memory_path() -> Path:
|
|
18
|
+
"""MEMORY.md lives in workspace root."""
|
|
19
|
+
workspace = Path(os.getenv("WORKSPACE_DIR", ".")).expanduser().resolve()
|
|
20
|
+
return workspace / "MEMORY.md"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_memory() -> str:
|
|
24
|
+
"""Read MEMORY.md content. Empty string if no memory yet."""
|
|
25
|
+
p = get_memory_path()
|
|
26
|
+
if not p.exists():
|
|
27
|
+
return ""
|
|
28
|
+
try:
|
|
29
|
+
return p.read_text(encoding="utf-8")
|
|
30
|
+
except Exception:
|
|
31
|
+
return ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def remember(note: str, category: str = "general") -> str:
|
|
35
|
+
"""Append a note to MEMORY.md under a category."""
|
|
36
|
+
p = get_memory_path()
|
|
37
|
+
timestamp = __import__("datetime").datetime.now().strftime("%Y-%m-%d")
|
|
38
|
+
|
|
39
|
+
# Read existing
|
|
40
|
+
existing = load_memory()
|
|
41
|
+
sections: dict[str, list[str]] = {}
|
|
42
|
+
current_section = None
|
|
43
|
+
|
|
44
|
+
for line in existing.splitlines():
|
|
45
|
+
if line.startswith("## "):
|
|
46
|
+
current_section = line[3:].strip().lower()
|
|
47
|
+
sections[current_section] = []
|
|
48
|
+
elif current_section is not None:
|
|
49
|
+
sections[current_section].append(line)
|
|
50
|
+
|
|
51
|
+
# Add new note
|
|
52
|
+
category = category.lower()
|
|
53
|
+
if category not in sections:
|
|
54
|
+
sections[category] = []
|
|
55
|
+
sections[category].append(f"- ({timestamp}) {note}")
|
|
56
|
+
|
|
57
|
+
# Rebuild
|
|
58
|
+
out_lines = [
|
|
59
|
+
"# Project Memory",
|
|
60
|
+
"",
|
|
61
|
+
"_Notes and conventions remembered by Oblivion across sessions._",
|
|
62
|
+
"",
|
|
63
|
+
]
|
|
64
|
+
for sec_name in sorted(sections.keys()):
|
|
65
|
+
out_lines.append(f"## {sec_name.title()}")
|
|
66
|
+
out_lines.append("")
|
|
67
|
+
for entry in sections[sec_name]:
|
|
68
|
+
if entry.strip():
|
|
69
|
+
out_lines.append(entry)
|
|
70
|
+
out_lines.append("")
|
|
71
|
+
|
|
72
|
+
p.write_text("\n".join(out_lines), encoding="utf-8")
|
|
73
|
+
return f"Remembered ({category}): {note}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def recall(category: str = None) -> str:
|
|
77
|
+
"""Get memory, optionally filtered by category."""
|
|
78
|
+
content = load_memory()
|
|
79
|
+
if not content:
|
|
80
|
+
return "No memory yet. The agent learns over time using remember()."
|
|
81
|
+
|
|
82
|
+
if category is None:
|
|
83
|
+
return content
|
|
84
|
+
|
|
85
|
+
# Filter by section
|
|
86
|
+
category = category.lower()
|
|
87
|
+
lines = content.splitlines()
|
|
88
|
+
capture = False
|
|
89
|
+
out = []
|
|
90
|
+
for line in lines:
|
|
91
|
+
if line.startswith("## "):
|
|
92
|
+
capture = line[3:].strip().lower() == category
|
|
93
|
+
if capture:
|
|
94
|
+
out.append(line)
|
|
95
|
+
return "\n".join(out) if out else f"No memory in category: {category}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_memory_summary() -> dict:
|
|
99
|
+
"""Quick stats about memory file."""
|
|
100
|
+
p = get_memory_path()
|
|
101
|
+
if not p.exists():
|
|
102
|
+
return {"exists": False, "notes": 0, "categories": 0}
|
|
103
|
+
content = load_memory()
|
|
104
|
+
notes = content.count("\n- ")
|
|
105
|
+
categories = content.count("\n## ")
|
|
106
|
+
return {
|
|
107
|
+
"exists": True,
|
|
108
|
+
"notes": notes,
|
|
109
|
+
"categories": categories,
|
|
110
|
+
"size_bytes": len(content.encode("utf-8")),
|
|
111
|
+
"path": str(p),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── VERIFICATION ──────────────────────────────────────────────────────────────
|
|
116
|
+
def verify_code(path: str, language: str = "auto") -> dict:
|
|
117
|
+
"""
|
|
118
|
+
Run a syntax check on a file.
|
|
119
|
+
Returns {ok: bool, message: str, details: str}.
|
|
120
|
+
"""
|
|
121
|
+
workspace = Path(os.getenv("WORKSPACE_DIR", ".")).expanduser().resolve()
|
|
122
|
+
p = workspace / path if not Path(path).is_absolute() else Path(path)
|
|
123
|
+
|
|
124
|
+
if not p.exists():
|
|
125
|
+
return {"ok": False, "message": f"File not found: {path}", "details": ""}
|
|
126
|
+
|
|
127
|
+
# Auto-detect language
|
|
128
|
+
if language == "auto":
|
|
129
|
+
suffix = p.suffix.lower()
|
|
130
|
+
lang_map = {
|
|
131
|
+
".py": "python", ".js": "javascript", ".ts": "typescript",
|
|
132
|
+
".jsx": "javascript", ".tsx": "typescript",
|
|
133
|
+
".json": "json", ".sh": "bash", ".yaml": "yaml", ".yml": "yaml",
|
|
134
|
+
}
|
|
135
|
+
language = lang_map.get(suffix, "unknown")
|
|
136
|
+
|
|
137
|
+
if language == "python":
|
|
138
|
+
return _verify_python(p)
|
|
139
|
+
elif language in ("javascript", "typescript"):
|
|
140
|
+
return _verify_js(p)
|
|
141
|
+
elif language == "json":
|
|
142
|
+
return _verify_json(p)
|
|
143
|
+
elif language == "bash":
|
|
144
|
+
return _verify_bash(p)
|
|
145
|
+
elif language in ("yaml", "yml"):
|
|
146
|
+
return _verify_yaml(p)
|
|
147
|
+
else:
|
|
148
|
+
return {
|
|
149
|
+
"ok": True,
|
|
150
|
+
"message": f"No verifier for {language}, skipping",
|
|
151
|
+
"details": ""
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _verify_python(p: Path) -> dict:
|
|
156
|
+
try:
|
|
157
|
+
result = subprocess.run(
|
|
158
|
+
["python3", "-m", "py_compile", str(p)],
|
|
159
|
+
capture_output=True, text=True, timeout=10,
|
|
160
|
+
)
|
|
161
|
+
if result.returncode == 0:
|
|
162
|
+
return {"ok": True, "message": "Python syntax OK", "details": ""}
|
|
163
|
+
return {
|
|
164
|
+
"ok": False,
|
|
165
|
+
"message": "Python syntax error",
|
|
166
|
+
"details": result.stderr.strip()[:500],
|
|
167
|
+
}
|
|
168
|
+
except subprocess.TimeoutExpired:
|
|
169
|
+
return {"ok": False, "message": "Verify timeout", "details": ""}
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return {"ok": False, "message": f"Verify error: {e}", "details": ""}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _verify_js(p: Path) -> dict:
|
|
175
|
+
if not _has_command("node"):
|
|
176
|
+
return {"ok": True, "message": "node not installed, skipping JS check", "details": ""}
|
|
177
|
+
try:
|
|
178
|
+
result = subprocess.run(
|
|
179
|
+
["node", "--check", str(p)],
|
|
180
|
+
capture_output=True, text=True, timeout=10,
|
|
181
|
+
)
|
|
182
|
+
if result.returncode == 0:
|
|
183
|
+
return {"ok": True, "message": "JS syntax OK", "details": ""}
|
|
184
|
+
return {
|
|
185
|
+
"ok": False,
|
|
186
|
+
"message": "JS syntax error",
|
|
187
|
+
"details": result.stderr.strip()[:500],
|
|
188
|
+
}
|
|
189
|
+
except Exception as e:
|
|
190
|
+
return {"ok": False, "message": f"Verify error: {e}", "details": ""}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _verify_json(p: Path) -> dict:
|
|
194
|
+
try:
|
|
195
|
+
json.loads(p.read_text(encoding="utf-8"))
|
|
196
|
+
return {"ok": True, "message": "JSON valid", "details": ""}
|
|
197
|
+
except json.JSONDecodeError as e:
|
|
198
|
+
return {"ok": False, "message": "JSON invalid", "details": str(e)[:500]}
|
|
199
|
+
except Exception as e:
|
|
200
|
+
return {"ok": False, "message": f"Verify error: {e}", "details": ""}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _verify_bash(p: Path) -> dict:
|
|
204
|
+
if not _has_command("bash"):
|
|
205
|
+
return {"ok": True, "message": "bash not found, skipping", "details": ""}
|
|
206
|
+
try:
|
|
207
|
+
result = subprocess.run(
|
|
208
|
+
["bash", "-n", str(p)],
|
|
209
|
+
capture_output=True, text=True, timeout=10,
|
|
210
|
+
)
|
|
211
|
+
if result.returncode == 0:
|
|
212
|
+
return {"ok": True, "message": "Bash syntax OK", "details": ""}
|
|
213
|
+
return {"ok": False, "message": "Bash syntax error", "details": result.stderr.strip()[:500]}
|
|
214
|
+
except Exception as e:
|
|
215
|
+
return {"ok": False, "message": f"Verify error: {e}", "details": ""}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _verify_yaml(p: Path) -> dict:
|
|
219
|
+
try:
|
|
220
|
+
import yaml
|
|
221
|
+
yaml.safe_load(p.read_text(encoding="utf-8"))
|
|
222
|
+
return {"ok": True, "message": "YAML valid", "details": ""}
|
|
223
|
+
except ImportError:
|
|
224
|
+
return {"ok": True, "message": "pyyaml not installed, skipping", "details": ""}
|
|
225
|
+
except Exception as e:
|
|
226
|
+
return {"ok": False, "message": "YAML invalid", "details": str(e)[:500]}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _has_command(name: str) -> bool:
|
|
230
|
+
import shutil
|
|
231
|
+
return shutil.which(name) is not None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ── PLANNING ──────────────────────────────────────────────────────────────────
|
|
235
|
+
@dataclass
|
|
236
|
+
class Plan:
|
|
237
|
+
goal: str
|
|
238
|
+
steps: list[str] = field(default_factory=list)
|
|
239
|
+
risks: list[str] = field(default_factory=list)
|
|
240
|
+
estimate: str = ""
|
|
241
|
+
approved: bool = False
|
|
242
|
+
|
|
243
|
+
def to_dict(self) -> dict:
|
|
244
|
+
return asdict(self)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Keywords that indicate a complex task needing planning
|
|
248
|
+
COMPLEX_KEYWORDS = [
|
|
249
|
+
"refactor", "rewrite", "restructure", "redesign", "architecture",
|
|
250
|
+
"migrate", "convert", "translate", "port",
|
|
251
|
+
"add error handling", "add tests", "add logging",
|
|
252
|
+
"across all", "in all files", "throughout", "everywhere",
|
|
253
|
+
"implement", "build a", "create a system",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def looks_complex(user_message: str) -> bool:
|
|
258
|
+
"""Heuristic: should this task trigger planning mode?"""
|
|
259
|
+
msg = user_message.lower()
|
|
260
|
+
# Long message hints at complexity
|
|
261
|
+
if len(msg.split()) > 25:
|
|
262
|
+
return True
|
|
263
|
+
# Contains complex keywords
|
|
264
|
+
for kw in COMPLEX_KEYWORDS:
|
|
265
|
+
if kw in msg:
|
|
266
|
+
return True
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def format_plan_panel(plan: Plan) -> str:
|
|
271
|
+
"""Format plan for display in chat (rich markup)."""
|
|
272
|
+
lines = [
|
|
273
|
+
f"[bold #00ff9f]Goal:[/bold #00ff9f] {plan.goal}",
|
|
274
|
+
"",
|
|
275
|
+
"[bold #00ff9f]Steps:[/bold #00ff9f]",
|
|
276
|
+
]
|
|
277
|
+
for i, step in enumerate(plan.steps, 1):
|
|
278
|
+
lines.append(f" {i}. {step}")
|
|
279
|
+
|
|
280
|
+
if plan.risks:
|
|
281
|
+
lines.append("")
|
|
282
|
+
lines.append("[bold #ff006e]Risks:[/bold #ff006e]")
|
|
283
|
+
for r in plan.risks:
|
|
284
|
+
lines.append(f" ⚠ {r}")
|
|
285
|
+
|
|
286
|
+
if plan.estimate:
|
|
287
|
+
lines.append("")
|
|
288
|
+
lines.append(f"[bold #b537f2]Estimate:[/bold #b537f2] {plan.estimate}")
|
|
289
|
+
|
|
290
|
+
return "\n".join(lines)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def format_plan_speech(plan: Plan, name: str = "boss") -> str:
|
|
294
|
+
"""Short version for FRIDAY to speak."""
|
|
295
|
+
n_steps = len(plan.steps)
|
|
296
|
+
parts = [f"Got a plan, {name}."]
|
|
297
|
+
parts.append(f"{n_steps} step{'s' if n_steps != 1 else ''}.")
|
|
298
|
+
if plan.risks:
|
|
299
|
+
parts.append(f"{len(plan.risks)} risk{'s' if len(plan.risks) != 1 else ''} to note.")
|
|
300
|
+
parts.append("Plan's on screen for review. Approve when ready.")
|
|
301
|
+
return " ".join(parts)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
305
|
+
# Context Manager — sliding window with summarization
|
|
306
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
307
|
+
"""
|
|
308
|
+
Stops conversation history from growing unbounded.
|
|
309
|
+
|
|
310
|
+
Strategy:
|
|
311
|
+
- Keep first user message (the original task)
|
|
312
|
+
- Keep last N messages verbatim (recent context)
|
|
313
|
+
- Summarize everything in between
|
|
314
|
+
|
|
315
|
+
Called by AgentRuntime when message count > THRESHOLD.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
import os
|
|
319
|
+
from typing import Callable, Optional
|
|
320
|
+
|
|
321
|
+
# Tuning knobs (override via env)
|
|
322
|
+
KEEP_LAST_N = int(os.getenv("CONTEXT_KEEP_LAST_N", "6"))
|
|
323
|
+
SUMMARIZE_THRESHOLD = int(os.getenv("CONTEXT_SUMMARIZE_THRESHOLD", "10"))
|
|
324
|
+
MAX_SUMMARY_TOKENS = int(os.getenv("CONTEXT_MAX_SUMMARY_TOKENS", "500"))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _approx_tokens(text: str) -> int:
|
|
328
|
+
"""Rough token estimate (1 token ≈ 4 chars)."""
|
|
329
|
+
return len(text) // 4
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def needs_compression(messages: list) -> bool:
|
|
333
|
+
"""Should we compress this conversation?"""
|
|
334
|
+
if len(messages) <= SUMMARIZE_THRESHOLD:
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
# Also compress if total tokens > 8000
|
|
338
|
+
total = sum(_approx_tokens(m.get("content", "")) for m in messages)
|
|
339
|
+
return total > 8000
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def compress_conversation(
|
|
343
|
+
messages: list,
|
|
344
|
+
summarize_fn: Optional[Callable[[str], str]] = None,
|
|
345
|
+
) -> list:
|
|
346
|
+
"""Compress middle of conversation, keep head + tail intact.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
messages: full conversation list
|
|
350
|
+
summarize_fn: callable that takes text and returns summary
|
|
351
|
+
(usually an LLM call). If None, uses naive truncation.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
new list with: [first user msg, summary msg, ...last N messages]
|
|
355
|
+
"""
|
|
356
|
+
if not needs_compression(messages):
|
|
357
|
+
return messages
|
|
358
|
+
|
|
359
|
+
if len(messages) < 2:
|
|
360
|
+
return messages
|
|
361
|
+
|
|
362
|
+
first = messages[0] # original task
|
|
363
|
+
last_n = messages[-KEEP_LAST_N:]
|
|
364
|
+
middle = messages[1:-KEEP_LAST_N]
|
|
365
|
+
|
|
366
|
+
if not middle:
|
|
367
|
+
return messages
|
|
368
|
+
|
|
369
|
+
# Format middle for summarization
|
|
370
|
+
middle_text = "\n\n".join(
|
|
371
|
+
f"[{m.get('role', '?').upper()}]: {m.get('content', '')[:500]}"
|
|
372
|
+
for m in middle
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if summarize_fn:
|
|
376
|
+
try:
|
|
377
|
+
summary = summarize_fn(middle_text)
|
|
378
|
+
except Exception:
|
|
379
|
+
# Fallback: naive truncation
|
|
380
|
+
summary = f"[Previous {len(middle)} messages truncated for brevity]"
|
|
381
|
+
else:
|
|
382
|
+
summary = f"[Previous {len(middle)} messages truncated for brevity]"
|
|
383
|
+
|
|
384
|
+
summary_msg = {
|
|
385
|
+
"role": "system",
|
|
386
|
+
"content": f"## CONVERSATION SUMMARY (so far)\n\n{summary}\n\n## RECENT MESSAGES:",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return [first, summary_msg] + last_n
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def summarize_via_llm(llm_client, text: str) -> str:
|
|
393
|
+
"""Use a cheap LLM call to summarize conversation text."""
|
|
394
|
+
prompt = f"""Summarize the following conversation history in 3-5 bullet points.
|
|
395
|
+
Focus on: what was attempted, what was decided, what files were created/modified, what errors occurred.
|
|
396
|
+
|
|
397
|
+
CONVERSATION:
|
|
398
|
+
{text[:6000]}
|
|
399
|
+
|
|
400
|
+
SUMMARY (bullets only, no preamble):"""
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
# Use a simple non-streaming chat call
|
|
404
|
+
if hasattr(llm_client, "chat"):
|
|
405
|
+
response = llm_client.chat(
|
|
406
|
+
messages=[{"role": "user", "content": prompt}],
|
|
407
|
+
stream=False,
|
|
408
|
+
)
|
|
409
|
+
return response.strip() if response else "[Summary unavailable]"
|
|
410
|
+
except Exception as e:
|
|
411
|
+
return f"[Summary failed: {type(e).__name__}]"
|
|
412
|
+
|
|
413
|
+
return "[Summary unavailable]"
|