eljay-ai 1.1.0

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 (64) hide show
  1. package/AGENTS.md +57 -0
  2. package/README.md +479 -0
  3. package/agent/__init__.py +10 -0
  4. package/agent/agent.py +269 -0
  5. package/agent/agents/__init__.py +34 -0
  6. package/agent/agents/registry.py +212 -0
  7. package/agent/builtin_tools.py +591 -0
  8. package/agent/chat.py +354 -0
  9. package/agent/config.py +67 -0
  10. package/agent/context.py +63 -0
  11. package/agent/edits.py +167 -0
  12. package/agent/gitaware.py +126 -0
  13. package/agent/hardware.py +331 -0
  14. package/agent/knowledge.py +133 -0
  15. package/agent/memory.py +98 -0
  16. package/agent/ollama_client.py +133 -0
  17. package/agent/permissions.py +93 -0
  18. package/agent/providers/__init__.py +51 -0
  19. package/agent/providers/base.py +78 -0
  20. package/agent/providers/image.py +110 -0
  21. package/agent/providers/ollama.py +111 -0
  22. package/agent/providers/video.py +96 -0
  23. package/agent/providers/web.py +155 -0
  24. package/agent/router.py +200 -0
  25. package/agent/rules.py +52 -0
  26. package/agent/runner.py +168 -0
  27. package/agent/skills.py +161 -0
  28. package/agent/tools.py +102 -0
  29. package/agent/verification.py +80 -0
  30. package/agent/workspace.py +487 -0
  31. package/bin/eljay +5 -0
  32. package/bin/eljay.cmd +4 -0
  33. package/bin/myagent +4 -0
  34. package/bin/myagent.cmd +4 -0
  35. package/eljay-ai-1.1.0.tgz +0 -0
  36. package/eljay.js +56 -0
  37. package/eljay.py +150 -0
  38. package/install.ps1 +28 -0
  39. package/knowledge/reference/diffusers.md +32 -0
  40. package/knowledge/reference/video-providers.md +21 -0
  41. package/knowledge/setup/comfyui.md +35 -0
  42. package/knowledge/setup/image-providers.md +16 -0
  43. package/knowledge/test-category/test-entry.md +5 -0
  44. package/myagent.py +30 -0
  45. package/package.json +40 -0
  46. package/skills/coding/code-review.md +3 -0
  47. package/skills/coding/fix-attempt-protocol.md +9 -0
  48. package/skills/debugging/root-cause-analysis.md +9 -0
  49. package/skills/general/communication.md +7 -0
  50. package/skills/laravel/authentication.md +15 -0
  51. package/skills/mysql/performance.md +9 -0
  52. package/skills/php/standards.md +8 -0
  53. package/skills/react/component-best-practices.md +8 -0
  54. package/skills/research/source-tracking.md +7 -0
  55. package/skills/security/input-validation.md +9 -0
  56. package/skills/testing/pytest-best-practices.md +7 -0
  57. package/tests/run_all.py +34 -0
  58. package/tests/test_agent_core.py +385 -0
  59. package/tests/test_capabilities.py +169 -0
  60. package/tests/test_edits.py +117 -0
  61. package/tests/test_eljay.py +209 -0
  62. package/tests/test_runner.py +114 -0
  63. package/tests/test_universal.py +458 -0
  64. package/tests/test_workspace.py +221 -0
@@ -0,0 +1,591 @@
1
+ """Built-in tools for ElJay AI.
2
+
3
+ This module registers every tool the agent can use. Tools are classified by
4
+ risk tier:
5
+
6
+ * ``RISK_SAFE`` — read-only, never prompts.
7
+ * ``RISK_ASK`` — needs user confirmation (file writes, commands).
8
+ * ``RISK_HIGH`` — dangerous (reserved for future delete/git-push).
9
+
10
+ Phases 4-6 added read-only project inspection. Phase 7 added safe file
11
+ editing. Phase 9 adds terminal execution, web research, git history, and
12
+ detection-only image/video generation.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from datetime import datetime
18
+ from typing import Any
19
+
20
+ from .edits import (
21
+ create_file as _create_file,
22
+ edit_file as _edit_file,
23
+ preview_create,
24
+ preview_edit,
25
+ )
26
+ from .gitaware import (
27
+ changed_files,
28
+ git_diff as _git_diff,
29
+ git_log as _git_log,
30
+ status as _git_status,
31
+ )
32
+ from .memory import Memory
33
+ from .providers import (
34
+ PROVIDERS,
35
+ ImageProvider,
36
+ OllamaProvider,
37
+ VideoProvider,
38
+ WebSearchProvider,
39
+ detect_ollama,
40
+ )
41
+ from .providers.web import fetch_web_page as _fetch_web_page
42
+ from .providers.web import search_ddg as _search_ddg
43
+ from .runner import classify_command
44
+ from .runner import run_command as _run_command
45
+ from .tools import RISK_ASK, RISK_HIGH, RISK_SAFE, Tool, ToolRegistry
46
+ from .verification import format_checks
47
+ from .workspace import Workspace
48
+
49
+ try:
50
+ from .agents import list_agents as _list_agent_defs
51
+ _has_agents = True
52
+ except Exception:
53
+ _has_agents = False
54
+ _list_agent_defs = None
55
+
56
+
57
+ def _as_int(value: Any, default: int, minimum: int, maximum: int | None = None) -> int:
58
+ try:
59
+ result = int(value)
60
+ except (TypeError, ValueError):
61
+ return default
62
+ if result < minimum:
63
+ result = minimum
64
+ if maximum is not None and result > maximum:
65
+ result = maximum
66
+ return result
67
+
68
+
69
+ def _as_bool(value: Any, default: bool = False) -> bool:
70
+ if isinstance(value, bool):
71
+ return value
72
+ if value is None:
73
+ return default
74
+ if isinstance(value, str):
75
+ return value.strip().lower() in ("1", "true", "yes", "y", "on")
76
+ return bool(value)
77
+
78
+
79
+ def _get_current_time() -> str:
80
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
81
+
82
+
83
+ def register_builtin_tools(
84
+ registry: ToolRegistry,
85
+ workspace: Workspace,
86
+ memory: Memory | None = None,
87
+ ) -> None:
88
+ # --- safe read-only tools ------------------------------------------
89
+
90
+ def project_info() -> str:
91
+ return workspace.summary()
92
+
93
+ def detect_checks() -> str:
94
+ return format_checks(workspace)
95
+
96
+ def git_status() -> str:
97
+ return _git_status(workspace)
98
+
99
+ def git_log(n: Any = 10) -> str:
100
+ return _git_log(workspace, _as_int(n, 10, 1, 50))
101
+
102
+ def git_diff(target: str = "", n: Any = 3) -> str:
103
+ return _git_diff(workspace, str(target), _as_int(n, 3, 1, 20))
104
+
105
+ def remember(text: str) -> str:
106
+ if memory is None:
107
+ return "ERROR: memory is not available"
108
+ memory.add_note(str(text))
109
+ return f"Remembered: {text}"
110
+
111
+ def list_files(path: str = ".", depth: int = 2, max_entries: int = 200) -> str:
112
+ return workspace.list_tree(
113
+ rel=str(path) if path is not None else ".",
114
+ depth=_as_int(depth, 2, 0, 12),
115
+ max_entries=_as_int(max_entries, 200, 1, 2000),
116
+ )
117
+
118
+ def read_file(path: str, start_line: Any = None, end_line: Any = None) -> str:
119
+ start = _as_int(start_line, 0, 0, 10 ** 9) or None
120
+ end = _as_int(end_line, 0, 0, 10 ** 9) or None
121
+ return workspace.read_file(str(path), start, end)
122
+
123
+ def search_code(
124
+ query: str,
125
+ path: str = ".",
126
+ regex: Any = False,
127
+ case_sensitive: Any = False,
128
+ max_results: Any = 50,
129
+ ) -> str:
130
+ return workspace.search(
131
+ query=str(query),
132
+ rel=str(path) if path is not None else ".",
133
+ regex=_as_bool(regex),
134
+ case_sensitive=_as_bool(case_sensitive),
135
+ max_results=_as_int(max_results, 50, 1, 500),
136
+ )
137
+
138
+ # --- write tools (risk: ask) --------------------------------------
139
+
140
+ def create_file(path: str, content: str) -> str:
141
+ return _create_file(workspace, str(path), content)
142
+
143
+ def edit_file(
144
+ path: str, old_text: str, new_text: str, replace_all: Any = False
145
+ ) -> str:
146
+ return _edit_file(
147
+ workspace, str(path), old_text, new_text, _as_bool(replace_all)
148
+ )
149
+
150
+ # --- command execution (risk: per-call) --------------------------
151
+
152
+ def run_command(command: str, cwd: str = ".", timeout: Any = 120) -> str:
153
+ return _run_command(
154
+ workspace,
155
+ str(command),
156
+ cwd=str(cwd) if cwd not in (None, "") else ".",
157
+ timeout=_as_int(timeout, 120, 1, 600),
158
+ )
159
+
160
+ def preview_run(command: str = "", cwd: str = ".", **_: Any) -> str:
161
+ risk = classify_command(str(command))
162
+ where = str(cwd) if cwd not in (None, "") else "."
163
+ return f"RUN (risk: {risk}) in {where}:\n $ {command}"
164
+
165
+ def risk_run(command: str = "", **_: Any) -> str:
166
+ return classify_command(str(command))
167
+
168
+ # --- web research tools (safe) ------------------------------------
169
+
170
+ def web_search(query: str, max_results: Any = 10) -> str:
171
+ results = _search_ddg(query=str(query), max_results=_as_int(max_results, 10, 1, 50))
172
+ if not results:
173
+ return "No search results returned."
174
+ lines = ["Search results for: " + str(query)]
175
+ for i, r in enumerate(results, 1):
176
+ if "error" in r:
177
+ lines.append(f" [{i}] (error) {r['error']}")
178
+ continue
179
+ url = r.get("url", "")
180
+ snippet = r.get("snippet", "(no snippet)")
181
+ lines.append(
182
+ f" [{i}] {r.get('title', '?')}\n"
183
+ f" URL: {url}\n"
184
+ f" {snippet[:200]}"
185
+ )
186
+ return "\n".join(lines)
187
+
188
+ def fetch_web_page(url: str, max_chars: Any = 8000) -> str:
189
+ text = _fetch_web_page(str(url), _as_int(max_chars, 8000, 100, 50000))
190
+ return text
191
+
192
+ # --- git history tools (safe) -------------------------------------
193
+
194
+ def get_git_log(n: Any = 10) -> str:
195
+ return _git_log(workspace, _as_int(n, 10, 1, 50))
196
+
197
+ def get_git_diff(target: str = "", n: Any = 3) -> str:
198
+ return _git_diff(workspace, str(target), _as_int(n, 3, 1, 20))
199
+
200
+ # --- provider / agent info (safe) ---------------------------------
201
+
202
+ def list_providers() -> str:
203
+ if not __import__("os").environ.get("AGENT_NO_PROVIDER_CHECK"):
204
+ for p in PROVIDERS:
205
+ p.is_available()
206
+ lines = ["Available providers:"]
207
+ for p in PROVIDERS:
208
+ lines.append(f" {p.status_string()}")
209
+ if not any(p.is_available() for p in PROVIDERS):
210
+ lines.append(" (none available)")
211
+ return "\n".join(lines)
212
+
213
+ def list_agents_func() -> str:
214
+ if not _has_agents or _list_agent_defs is None:
215
+ return "Agent registry not available."
216
+ lines = ["Specialized agents:"]
217
+ for agent in sorted(_list_agent_defs(), key=lambda a: (-a.priority, a.name)):
218
+ tools_list = ", ".join(agent.tools[:8])
219
+ if len(agent.tools) > 8:
220
+ tools_list += f" (+{len(agent.tools) - 8} more)"
221
+ lines.append(f" {agent.name:12s} — {agent.purpose}")
222
+ lines.append(f" tools: {tools_list}")
223
+ lines.append(f" providers: {', '.join(agent.providers) or 'none'}")
224
+ return "\n".join(lines)
225
+
226
+ # --- image / video generation (detection only) --------------------
227
+
228
+ def _check_image_provider() -> tuple[ImageProvider, bool, str]:
229
+ provider = ImageProvider()
230
+ ok = provider.is_available()
231
+ return provider, ok, provider.error or ""
232
+
233
+ def generate_image(prompt: str, style: str = "", steps: Any = 20) -> str:
234
+ provider, ok, err = _check_image_provider()
235
+ if not ok:
236
+ return (
237
+ f"[unavailable] No local image-generation backend found.\n{err}\n"
238
+ "Install one of: ComfyUI, or `pip install diffusers[torch]`."
239
+ )
240
+ result = provider.execute("image-generation", prompt=prompt, style=style, steps=steps)
241
+ # If the provider returned a placeholder, surface it honestly.
242
+ if result.startswith("[ComfyUI]") or result.startswith("[diffusers]"):
243
+ return result
244
+ return f"[{provider.model}] {result}"
245
+
246
+ def _check_video_provider() -> tuple[VideoProvider, bool, str]:
247
+ provider = VideoProvider()
248
+ ok = provider.is_available()
249
+ return provider, ok, provider.error or ""
250
+
251
+ def generate_video(prompt: str, duration: Any = 4) -> str:
252
+ provider, ok, err = _check_video_provider()
253
+ if not ok:
254
+ return (
255
+ f"[unavailable] Video generation is not available on this system.\n{err}\n"
256
+ "Local video generation requires a GPU with >= 6 GB VRAM and a backend "
257
+ "like ComfyUI + AnimateDiff or diffusers + animatediff. "
258
+ "Cloud services are an alternative for this task."
259
+ )
260
+ return provider.execute("video-generation", prompt=prompt, duration=duration)
261
+
262
+ # -- previews for confirmation ---------------------------------------
263
+
264
+ def preview_create_callable(path: str = "", content: str = "", **_: Any) -> str:
265
+ return preview_create(str(path), content)
266
+
267
+ def preview_edit_callable(
268
+ path: str = "",
269
+ old_text: str = "",
270
+ new_text: str = "",
271
+ replace_all: Any = False,
272
+ **_: Any,
273
+ ) -> str:
274
+ return preview_edit(str(path), old_text, new_text, _as_bool(replace_all))
275
+
276
+ # -- registration ----------------------------------------------------
277
+
278
+ _register_get_current_time(registry)
279
+ _register_project_info(registry, project_info)
280
+ _register_detect_checks(registry, detect_checks)
281
+ _register_git_status(registry, git_status)
282
+ _register_git_log(registry, get_git_log)
283
+ _register_git_diff(registry, get_git_diff)
284
+ _register_remember(registry, remember)
285
+ _register_list_files(registry, list_files)
286
+ _register_read_file(registry, read_file)
287
+ _register_search_code(registry, search_code)
288
+ _register_create_file(registry, create_file, preview_create_callable)
289
+ _register_edit_file(registry, edit_file, preview_edit_callable)
290
+ _register_run_command(registry, run_command, preview_run, risk_run)
291
+ _register_web_search(registry, web_search)
292
+ _register_fetch_web_page(registry, fetch_web_page)
293
+ _register_list_providers(registry, list_providers)
294
+ _register_list_agents(registry, list_agents_func)
295
+ _register_generate_image(registry, generate_image)
296
+ _register_generate_video(registry, generate_video)
297
+
298
+
299
+ # ---------------------------------------------------------------------------
300
+ # Individual registration helpers (keeps the function above clean and makes
301
+ # it easy to see each tool's schema + risk at a glance).
302
+ # ---------------------------------------------------------------------------
303
+
304
+ def _register_get_current_time(registry: ToolRegistry) -> None:
305
+ registry.register(Tool(
306
+ name="get_current_time",
307
+ description="Return the current local date and time on the user's machine.",
308
+ parameters={"type": "object", "properties": {}, "required": []},
309
+ func=_get_current_time,
310
+ risk=RISK_SAFE,
311
+ ))
312
+
313
+
314
+ def _register_project_info(registry, func):
315
+ registry.register(Tool(
316
+ name="project_info",
317
+ description="Describe the current project: name, root folder, detected technologies.",
318
+ parameters={"type": "object", "properties": {}, "required": []},
319
+ func=func,
320
+ risk=RISK_SAFE,
321
+ ))
322
+
323
+
324
+ def _register_detect_checks(registry, func):
325
+ registry.register(Tool(
326
+ name="detect_checks",
327
+ description="List the project's verification commands (lint, test, build), smallest first.",
328
+ parameters={"type": "object", "properties": {}, "required": []},
329
+ func=func,
330
+ risk=RISK_SAFE,
331
+ ))
332
+
333
+
334
+ def _register_git_status(registry, func):
335
+ registry.register(Tool(
336
+ name="git_status",
337
+ description="Show the git branch and changed files (read-only). Never commits or pushes.",
338
+ parameters={"type": "object", "properties": {}, "required": []},
339
+ func=func,
340
+ risk=RISK_SAFE,
341
+ ))
342
+
343
+
344
+ def _register_git_log(registry, func):
345
+ registry.register(Tool(
346
+ name="git_log",
347
+ description="Show recent git commit history (read-only, up to 50 entries).",
348
+ parameters={
349
+ "type": "object",
350
+ "properties": {"n": {"type": "integer", "description": "Number of commits to show (1-50). Default 10."}},
351
+ "required": [],
352
+ },
353
+ func=func,
354
+ risk=RISK_SAFE,
355
+ ))
356
+
357
+
358
+ def _register_git_diff(registry, func):
359
+ registry.register(Tool(
360
+ name="git_diff",
361
+ description="Show uncommitted changes or diff for a target (read-only).",
362
+ parameters={
363
+ "type": "object",
364
+ "properties": {
365
+ "target": {"type": "string", "description": "File or branch to diff against. Default: uncommitted changes."},
366
+ "n": {"type": "integer", "description": "Max number of lines. Default 3."},
367
+ },
368
+ "required": [],
369
+ },
370
+ func=func,
371
+ risk=RISK_SAFE,
372
+ ))
373
+
374
+
375
+ def _register_remember(registry, func):
376
+ registry.register(Tool(
377
+ name="remember",
378
+ description="Save a fact to local memory (.agent/) for future recall.",
379
+ parameters={
380
+ "type": "object",
381
+ "properties": {
382
+ "text": {"type": "string", "description": "The fact to remember, one short sentence."}
383
+ },
384
+ "required": ["text"],
385
+ },
386
+ func=func,
387
+ risk=RISK_SAFE,
388
+ ))
389
+
390
+
391
+ def _register_list_files(registry, func):
392
+ registry.register(Tool(
393
+ name="list_files",
394
+ description="List files and folders in the project (read-only). Skips build/cache folders.",
395
+ parameters={
396
+ "type": "object",
397
+ "properties": {
398
+ "path": {"type": "string", "description": "Folder to list, relative to root. Defaults to root."},
399
+ "depth": {"type": "integer", "description": "Folder levels to show (0-12). Default 2."},
400
+ "max_entries": {"type": "integer", "description": "Max entries (1-2000). Default 200."},
401
+ },
402
+ "required": [],
403
+ },
404
+ func=func,
405
+ risk=RISK_SAFE,
406
+ ))
407
+
408
+
409
+ def _register_read_file(registry, func):
410
+ registry.register(Tool(
411
+ name="read_file",
412
+ description="Read a text file (read-only) with line numbers. Binary files refused.",
413
+ parameters={
414
+ "type": "object",
415
+ "properties": {
416
+ "path": {"type": "string", "description": "File to read, relative to project root."},
417
+ "start_line": {"type": "integer", "description": "First line (1-based). Optional."},
418
+ "end_line": {"type": "integer", "description": "Last line (inclusive). Optional."},
419
+ },
420
+ "required": ["path"],
421
+ },
422
+ func=func,
423
+ risk=RISK_SAFE,
424
+ ))
425
+
426
+
427
+ def _register_search_code(registry, func):
428
+ registry.register(Tool(
429
+ name="search_code",
430
+ description="Search project files for text/regex. Returns path:line: snippet.",
431
+ parameters={
432
+ "type": "object",
433
+ "properties": {
434
+ "query": {"type": "string", "description": "Text to find (or regex if regex=true)."},
435
+ "path": {"type": "string", "description": "Folder/file to search. Defaults to whole project."},
436
+ "regex": {"type": "boolean", "description": "Treat query as regex. Default false."},
437
+ "case_sensitive": {"type": "boolean", "description": "Exact case. Default false."},
438
+ "max_results": {"type": "integer", "description": "Max matches (1-500). Default 50."},
439
+ },
440
+ "required": ["query"],
441
+ },
442
+ func=func,
443
+ risk=RISK_SAFE,
444
+ ))
445
+
446
+
447
+ def _register_create_file(registry, func, preview):
448
+ registry.register(Tool(
449
+ name="create_file",
450
+ description="Create a NEW file. Cannot overwrite existing files. Requires approval.",
451
+ parameters={
452
+ "type": "object",
453
+ "properties": {
454
+ "path": {"type": "string", "description": "New file path, relative to project root."},
455
+ "content": {"type": "string", "description": "Full content of the new file."},
456
+ },
457
+ "required": ["path", "content"],
458
+ },
459
+ func=func,
460
+ risk=RISK_ASK,
461
+ preview=preview,
462
+ ))
463
+
464
+
465
+ def _register_edit_file(registry, func, preview):
466
+ registry.register(Tool(
467
+ name="edit_file",
468
+ description="Replace exact text in an existing file. Requires approval.",
469
+ parameters={
470
+ "type": "object",
471
+ "properties": {
472
+ "path": {"type": "string", "description": "File to edit."},
473
+ "old_text": {"type": "string", "description": "Exact text to replace."},
474
+ "new_text": {"type": "string", "description": "Replacement text."},
475
+ "replace_all": {"type": "boolean", "description": "Replace all occurrences. Default false."},
476
+ },
477
+ "required": ["path", "old_text", "new_text"],
478
+ },
479
+ func=func,
480
+ risk=RISK_ASK,
481
+ preview=preview,
482
+ ))
483
+
484
+
485
+ def _register_run_command(registry, func, preview, risk_func):
486
+ registry.register(Tool(
487
+ name="run_command",
488
+ description="Run ONE terminal command in the project. No pipes/redirects. Shown for approval first.",
489
+ parameters={
490
+ "type": "object",
491
+ "properties": {
492
+ "command": {"type": "string", "description": "The single command to run."},
493
+ "cwd": {"type": "string", "description": "Working dir relative to root. Defaults to root."},
494
+ "timeout": {"type": "integer", "description": "Seconds (1-600). Default 120."},
495
+ },
496
+ "required": ["command"],
497
+ },
498
+ func=func,
499
+ risk=RISK_ASK,
500
+ preview=preview,
501
+ risk_func=risk_func,
502
+ ))
503
+
504
+
505
+ def _register_web_search(registry, func):
506
+ registry.register(Tool(
507
+ name="web_search",
508
+ description="Search the web via DuckDuckGo Lite (free, no API key). Returns titles, URLs, snippets.",
509
+ parameters={
510
+ "type": "object",
511
+ "properties": {
512
+ "query": {"type": "string", "description": "Search query."},
513
+ "max_results": {"type": "integer", "description": "Max results (1-50). Default 10."},
514
+ },
515
+ "required": ["query"],
516
+ },
517
+ func=func,
518
+ risk=RISK_SAFE,
519
+ ))
520
+
521
+
522
+ def _register_fetch_web_page(registry, func):
523
+ registry.register(Tool(
524
+ name="fetch_web_page",
525
+ description="Fetch a URL and return readable text. Respects rate limits; no paywall bypass.",
526
+ parameters={
527
+ "type": "object",
528
+ "properties": {
529
+ "url": {"type": "string", "description": "Full http:// or https:// URL."},
530
+ "max_chars": {"type": "integer", "description": "Max characters to return. Default 8000."},
531
+ },
532
+ "required": ["url"],
533
+ },
534
+ func=func,
535
+ risk=RISK_SAFE,
536
+ ))
537
+
538
+
539
+ def _register_list_providers(registry, func):
540
+ registry.register(Tool(
541
+ name="list_providers",
542
+ description="List all AI providers and their availability status.",
543
+ parameters={"type": "object", "properties": {}, "required": []},
544
+ func=func,
545
+ risk=RISK_SAFE,
546
+ ))
547
+
548
+
549
+ def _register_list_agents(registry, func):
550
+ registry.register(Tool(
551
+ name="list_agents",
552
+ description="List all specialized agents (Coding, Research, Image, Video, Knowledge) and their capabilities.",
553
+ parameters={"type": "object", "properties": {}, "required": []},
554
+ func=func,
555
+ risk=RISK_SAFE,
556
+ ))
557
+
558
+
559
+ def _register_generate_image(registry, func):
560
+ registry.register(Tool(
561
+ name="generate_image",
562
+ description="Generate an image from a text prompt using local providers. Reports availability honestly.",
563
+ parameters={
564
+ "type": "object",
565
+ "properties": {
566
+ "prompt": {"type": "string", "description": "Detailed image description."},
567
+ "style": {"type": "string", "description": "Optional style modifier. Default empty."},
568
+ "steps": {"type": "integer", "description": "Generation steps. Default 20."},
569
+ },
570
+ "required": ["prompt"],
571
+ },
572
+ func=func,
573
+ risk=RISK_SAFE,
574
+ ))
575
+
576
+
577
+ def _register_generate_video(registry, func):
578
+ registry.register(Tool(
579
+ name="generate_video",
580
+ description="Generate video from a text prompt. Reports hardware requirements honestly.",
581
+ parameters={
582
+ "type": "object",
583
+ "properties": {
584
+ "prompt": {"type": "string", "description": "Detailed video description."},
585
+ "duration": {"type": "integer", "description": "Duration in seconds. Default 4."},
586
+ },
587
+ "required": ["prompt"],
588
+ },
589
+ func=func,
590
+ risk=RISK_SAFE,
591
+ ))