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.
Files changed (74) hide show
  1. agent/__init__.py +0 -0
  2. agent/brain.py +413 -0
  3. agent/code_chunker.py +431 -0
  4. agent/core.py +709 -0
  5. agent/core.py.backup +243 -0
  6. agent/core.py.bak-complex +513 -0
  7. agent/core.py.bak-prefix +423 -0
  8. agent/core.py.bak-workspace +467 -0
  9. agent/core.py.before_knowledge_patch +605 -0
  10. agent/friday.py +455 -0
  11. agent/llm.py +265 -0
  12. agent/models.py +156 -0
  13. agent/parser.py +218 -0
  14. agent/parser.py.bak-prefix +176 -0
  15. agent/paths.py +235 -0
  16. agent/rag.py +477 -0
  17. agent/runtime.py +467 -0
  18. agent/runtime.py.bak-complex +299 -0
  19. agent/runtime.py.bak-prefix +280 -0
  20. agent/runtime.py.before_knowledge_patch +384 -0
  21. agent/setup_wizard.py +234 -0
  22. agent/symbol_index.py +314 -0
  23. agent/voice.py +343 -0
  24. agent/watcher.py +176 -0
  25. db/__init__.py +0 -0
  26. db/store.py +188 -0
  27. knowledge/__init__.py +0 -0
  28. knowledge/detector.py +162 -0
  29. knowledge/injector.py +175 -0
  30. knowledge/packs/database.md +630 -0
  31. knowledge/packs/debugging.md +96 -0
  32. knowledge/packs/deployment.md +72 -0
  33. knowledge/packs/django.md +285 -0
  34. knowledge/packs/docker.md +699 -0
  35. knowledge/packs/fastapi.md +534 -0
  36. knowledge/packs/nextjs.md +129 -0
  37. knowledge/packs/react.md +142 -0
  38. knowledge/packs/security.md +882 -0
  39. knowledge/packs/tailwind.md +107 -0
  40. knowledge/packs/testing.md +718 -0
  41. knowledge/packs/typescript.md +599 -0
  42. knowledge/packs/vue.md +123 -0
  43. mcp_server/__init__.py +0 -0
  44. mcp_server/server.py +160 -0
  45. oblivion_agent-2.9.0.dist-info/METADATA +140 -0
  46. oblivion_agent-2.9.0.dist-info/RECORD +74 -0
  47. oblivion_agent-2.9.0.dist-info/WHEEL +4 -0
  48. oblivion_agent-2.9.0.dist-info/entry_points.txt +2 -0
  49. oblivion_agent-2.9.0.dist-info/licenses/LICENSE +21 -0
  50. tools/__init__.py +0 -0
  51. tools/auto.py +240 -0
  52. tools/bash.py +169 -0
  53. tools/bash.py.bak-complex +29 -0
  54. tools/diff.py +75 -0
  55. tools/edit_file.py +77 -0
  56. tools/filesystem.py +270 -0
  57. tools/planner.py +53 -0
  58. tools/registry.py +291 -0
  59. tools/registry.py.bak-complex +250 -0
  60. tools/registry.py.bak-workspace +234 -0
  61. tools/search_code.py +127 -0
  62. tools/symbol_tools.py +212 -0
  63. ui/__init__.py +0 -0
  64. ui/app.py +2332 -0
  65. ui/app.py.backup +1634 -0
  66. ui/app.py.backup.20260612_181912 +1886 -0
  67. ui/app.py.bak-complex +1981 -0
  68. ui/app.py.bak-workspace +1907 -0
  69. ui/app.py.before_dedupe.20260612_110555 +1910 -0
  70. ui/app.py.before_dedupe.20260612_110629 +1797 -0
  71. ui/app.py.before_dockfix.20260612_112427 +1797 -0
  72. ui/app.py.before_wavefix.20260612_112136 +1797 -0
  73. ui/app.py.cyberpunk_backup +2011 -0
  74. 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]"