connectonion 0.5.8__tar.gz → 0.5.10__tar.gz
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.
- {connectonion-0.5.8 → connectonion-0.5.10}/PKG-INFO +1 -1
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/__init__.py +3 -1
- connectonion-0.5.10/connectonion/cli/commands/copy_commands.py +116 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/main.py +14 -1
- connectonion-0.5.10/connectonion/connect.py +272 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/host.py +31 -14
- connectonion-0.5.10/connectonion/transcribe.py +245 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/cli/README.md +47 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/useful_plugins/README.md +24 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/useful_tools/README.md +24 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/pyproject.toml +1 -1
- connectonion-0.5.8/connectonion/connect.py +0 -128
- {connectonion-0.5.8 → connectonion-0.5.10}/.gitignore +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/address.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/agent.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/announce.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/asgi.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/auto_debug_exception.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/browser_agent/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/browser_agent/browser.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/browser_agent/prompt.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/auth_commands.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/browser_commands.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/create.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/deploy_commands.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/doctor_commands.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/init.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/project_cmd_lib.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/reset_commands.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/commands/status_commands.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/docs/connectonion.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/docs.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/agent.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/prompts/metagent.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/meta-agent/prompts/think_prompt.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/minimal/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/minimal/agent.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/playwright/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/playwright/agent.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/playwright/prompt.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/playwright/requirements.txt +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/cli/templates/web-research/agent.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/console.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_agent/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_agent/agent.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_agent/prompts/debug_assistant.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_agent/runtime_inspector.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/explain_agent.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/explain_context.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/explainer_prompt.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debug_explainer/root_cause_analysis_prompt.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/debugger_ui.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/decorators.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/events.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/execution_analyzer/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/execution_analyzer/execution_analysis.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/execution_analyzer/execution_analysis_prompt.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/interactive_debugger.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/llm.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/llm_do.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/logger.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/analyze_contact.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/eval_expected.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/react_evaluate.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/react_plan.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompt_files/reflect.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/prompts.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/relay.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/static/docs.html +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tool_executor.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tool_factory.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tool_registry.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/trust.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/trust_agents.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/trust_functions.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/divider.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/dropdown.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/footer.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/fuzzy.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/input.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/keys.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/pick.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/providers.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/tui/status_bar.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/usage.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_events_handlers/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_events_handlers/reflect.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/calendar_plugin.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/eval.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/gmail_plugin.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/image_result_formatter.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/re_act.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_plugins/shell_approval.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/__init__.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/diff_writer.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/get_emails.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/gmail.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/google_calendar.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/memory.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/microsoft_calendar.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/outlook.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/send_email.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/shell.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/slash_command.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/terminal.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/todo_list.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/useful_tools/web_fetch.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/connectonion/xray.py +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/debug/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/integrations/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/network/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/templates/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/docs/tui/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/examples/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/examples/browser-agent/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/examples/email-agent/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/examples/simple-agent/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/prompts/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/prompts/formats/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/tests/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/tests/cli/README.md +0 -0
- {connectonion-0.5.8 → connectonion-0.5.10}/tests/cli/aws/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: connectonion
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.10
|
|
4
4
|
Summary: A simple Python framework for creating AI agents with behavior tracking
|
|
5
5
|
Project-URL: Homepage, https://github.com/openonion/connectonion
|
|
6
6
|
Project-URL: Documentation, https://docs.connectonion.com
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""ConnectOnion - A simple agent framework with behavior tracking."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.5.
|
|
3
|
+
__version__ = "0.5.10"
|
|
4
4
|
|
|
5
5
|
# Auto-load .env files for the entire framework
|
|
6
6
|
from dotenv import load_dotenv
|
|
@@ -15,6 +15,7 @@ from .tool_factory import create_tool_from_function
|
|
|
15
15
|
from .llm import LLM
|
|
16
16
|
from .logger import Logger
|
|
17
17
|
from .llm_do import llm_do
|
|
18
|
+
from .transcribe import transcribe
|
|
18
19
|
from .prompts import load_system_prompt
|
|
19
20
|
from .xray import xray
|
|
20
21
|
from .decorators import replay, xray_replay
|
|
@@ -40,6 +41,7 @@ __all__ = [
|
|
|
40
41
|
"Logger",
|
|
41
42
|
"create_tool_from_function",
|
|
42
43
|
"llm_do",
|
|
44
|
+
"transcribe",
|
|
43
45
|
"load_system_prompt",
|
|
44
46
|
"xray",
|
|
45
47
|
"replay",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: CLI command to copy built-in tools and plugins to user's project for customization
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [shutil, pathlib, typing, rich] | imported by [cli/main.py via handle_copy()]
|
|
5
|
+
Data flow: user runs `co copy <name>` → looks up name in TOOLS/PLUGINS registry → finds source via module.__file__ → copies to ./tools/ or ./plugins/
|
|
6
|
+
State/Effects: creates tools/ or plugins/ directory if needed | copies .py files from installed package to user's project
|
|
7
|
+
Integration: exposes handle_copy() for CLI | uses Python import system to find installed package location (cross-platform)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, List
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# Registry of copyable tools
|
|
19
|
+
TOOLS = {
|
|
20
|
+
"gmail": "gmail.py",
|
|
21
|
+
"outlook": "outlook.py",
|
|
22
|
+
"google_calendar": "google_calendar.py",
|
|
23
|
+
"microsoft_calendar": "microsoft_calendar.py",
|
|
24
|
+
"memory": "memory.py",
|
|
25
|
+
"web_fetch": "web_fetch.py",
|
|
26
|
+
"shell": "shell.py",
|
|
27
|
+
"diff_writer": "diff_writer.py",
|
|
28
|
+
"todo_list": "todo_list.py",
|
|
29
|
+
"slash_command": "slash_command.py",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Registry of copyable plugins
|
|
33
|
+
PLUGINS = {
|
|
34
|
+
"re_act": "re_act.py",
|
|
35
|
+
"eval": "eval.py",
|
|
36
|
+
"image_result_formatter": "image_result_formatter.py",
|
|
37
|
+
"shell_approval": "shell_approval.py",
|
|
38
|
+
"gmail_plugin": "gmail_plugin.py",
|
|
39
|
+
"calendar_plugin": "calendar_plugin.py",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def handle_copy(
|
|
44
|
+
names: List[str],
|
|
45
|
+
list_all: bool = False,
|
|
46
|
+
path: Optional[str] = None,
|
|
47
|
+
force: bool = False
|
|
48
|
+
):
|
|
49
|
+
"""Copy built-in tools and plugins to user's project."""
|
|
50
|
+
|
|
51
|
+
# Show list if requested or no names provided
|
|
52
|
+
if list_all or not names:
|
|
53
|
+
show_available_items()
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# Get source directories using import system (works for installed packages)
|
|
57
|
+
import connectonion.useful_tools as tools_module
|
|
58
|
+
import connectonion.useful_plugins as plugins_module
|
|
59
|
+
|
|
60
|
+
useful_tools_dir = Path(tools_module.__file__).parent
|
|
61
|
+
useful_plugins_dir = Path(plugins_module.__file__).parent
|
|
62
|
+
|
|
63
|
+
current_dir = Path.cwd()
|
|
64
|
+
|
|
65
|
+
for name in names:
|
|
66
|
+
name_lower = name.lower()
|
|
67
|
+
|
|
68
|
+
# Check if it's a tool
|
|
69
|
+
if name_lower in TOOLS:
|
|
70
|
+
source = useful_tools_dir / TOOLS[name_lower]
|
|
71
|
+
dest_dir = Path(path) if path else current_dir / "tools"
|
|
72
|
+
copy_file(source, dest_dir, force)
|
|
73
|
+
|
|
74
|
+
# Check if it's a plugin
|
|
75
|
+
elif name_lower in PLUGINS:
|
|
76
|
+
source = useful_plugins_dir / PLUGINS[name_lower]
|
|
77
|
+
dest_dir = Path(path) if path else current_dir / "plugins"
|
|
78
|
+
copy_file(source, dest_dir, force)
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
console.print(f"[red]Unknown: {name}[/red]")
|
|
82
|
+
console.print("Use [cyan]co copy --list[/cyan] to see available items")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def copy_file(source: Path, dest_dir: Path, force: bool):
|
|
86
|
+
"""Copy a single file to destination."""
|
|
87
|
+
if not source.exists():
|
|
88
|
+
console.print(f"[red]Source not found: {source}[/red]")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
dest = dest_dir / source.name
|
|
93
|
+
|
|
94
|
+
if dest.exists() and not force:
|
|
95
|
+
console.print(f"[yellow]Skipped: {dest} (exists, use --force)[/yellow]")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
shutil.copy2(source, dest)
|
|
99
|
+
console.print(f"[green]✓ Copied: {dest}[/green]")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def show_available_items():
|
|
103
|
+
"""Display available tools and plugins."""
|
|
104
|
+
table = Table(title="Available Items to Copy")
|
|
105
|
+
table.add_column("Name", style="cyan")
|
|
106
|
+
table.add_column("Type", style="green")
|
|
107
|
+
table.add_column("File")
|
|
108
|
+
|
|
109
|
+
for name, file in sorted(TOOLS.items()):
|
|
110
|
+
table.add_row(name, "tool", file)
|
|
111
|
+
|
|
112
|
+
for name, file in sorted(PLUGINS.items()):
|
|
113
|
+
table.add_row(name, "plugin", file)
|
|
114
|
+
|
|
115
|
+
console.print(table)
|
|
116
|
+
console.print("\n[dim]Usage: co copy <name> [--path ./custom/][/dim]")
|
|
@@ -11,7 +11,7 @@ LLM-Note:
|
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
13
|
from rich.console import Console
|
|
14
|
-
from typing import Optional
|
|
14
|
+
from typing import Optional, List
|
|
15
15
|
|
|
16
16
|
from .. import __version__
|
|
17
17
|
|
|
@@ -54,6 +54,7 @@ def _show_help():
|
|
|
54
54
|
console.print("[bold]Commands:[/bold]")
|
|
55
55
|
console.print(" [green]create[/green] <name> Create new project")
|
|
56
56
|
console.print(" [green]init[/green] Initialize in current directory")
|
|
57
|
+
console.print(" [green]copy[/green] <name> Copy tool/plugin source to project")
|
|
57
58
|
console.print(" [green]deploy[/green] Deploy to ConnectOnion Cloud")
|
|
58
59
|
console.print(" [green]auth[/green] Authenticate for managed keys")
|
|
59
60
|
console.print(" [green]status[/green] Check account balance")
|
|
@@ -139,6 +140,18 @@ def browser(command: str = typer.Argument(..., help="Browser command")):
|
|
|
139
140
|
handle_browser(command)
|
|
140
141
|
|
|
141
142
|
|
|
143
|
+
@app.command()
|
|
144
|
+
def copy(
|
|
145
|
+
names: List[str] = typer.Argument(None, help="Tool or plugin names to copy"),
|
|
146
|
+
list_all: bool = typer.Option(False, "--list", "-l", help="List available items"),
|
|
147
|
+
path: Optional[str] = typer.Option(None, "--path", "-p", help="Custom destination path"),
|
|
148
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
|
149
|
+
):
|
|
150
|
+
"""Copy built-in tools/plugins to customize."""
|
|
151
|
+
from .commands.copy_commands import handle_copy
|
|
152
|
+
handle_copy(names=names or [], list_all=list_all, path=path, force=force)
|
|
153
|
+
|
|
154
|
+
|
|
142
155
|
def cli():
|
|
143
156
|
"""Entry point."""
|
|
144
157
|
app()
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Client interface for connecting to remote agents via HTTP or relay network
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [asyncio, json, uuid, time, aiohttp, websockets, address] | imported by [__init__.py, tests/test_connect.py, examples/] | tested by [tests/test_connect.py]
|
|
5
|
+
Data flow: connect(address, keys) → RemoteAgent → input() → discover endpoints → try HTTP first → fallback to relay → return result
|
|
6
|
+
State/Effects: caches discovered endpoint for reuse | optional signing with keys parameter
|
|
7
|
+
Integration: exposes connect(address, keys, relay_url), RemoteAgent class with .input(), .input_async()
|
|
8
|
+
Performance: discovery cached per RemoteAgent instance | HTTPS tried first (direct), relay as fallback
|
|
9
|
+
|
|
10
|
+
Connect to remote agents on the network.
|
|
11
|
+
|
|
12
|
+
Smart discovery: tries HTTP endpoints first, falls back to relay.
|
|
13
|
+
Always signs requests when keys are provided.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from . import address as addr
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RemoteAgent:
|
|
26
|
+
"""
|
|
27
|
+
Interface to a remote agent.
|
|
28
|
+
|
|
29
|
+
Supports:
|
|
30
|
+
- Discovery via relay API
|
|
31
|
+
- Direct HTTP POST to agent /input endpoint
|
|
32
|
+
- WebSocket relay fallback
|
|
33
|
+
- Signed requests when keys provided
|
|
34
|
+
- Multi-turn conversations via session management
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
# Standard Python scripts
|
|
38
|
+
agent = connect("0x...")
|
|
39
|
+
result = agent.input("Hello")
|
|
40
|
+
|
|
41
|
+
# Jupyter notebooks or async code
|
|
42
|
+
agent = connect("0x...")
|
|
43
|
+
result = await agent.input_async("Hello")
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
agent_address: str,
|
|
49
|
+
*,
|
|
50
|
+
keys: Optional[Dict[str, Any]] = None,
|
|
51
|
+
relay_url: str = "wss://oo.openonion.ai/ws/announce"
|
|
52
|
+
):
|
|
53
|
+
self.address = agent_address
|
|
54
|
+
self._keys = keys
|
|
55
|
+
self._relay_url = relay_url
|
|
56
|
+
self._cached_endpoint: Optional[str] = None
|
|
57
|
+
self._session: Optional[Dict[str, Any]] = None # Multi-turn conversation state
|
|
58
|
+
|
|
59
|
+
def input(self, prompt: str, timeout: float = 30.0) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Send task to remote agent and get response (sync version).
|
|
62
|
+
|
|
63
|
+
Automatically maintains conversation context across calls.
|
|
64
|
+
|
|
65
|
+
Note:
|
|
66
|
+
This method cannot be used inside an async context (e.g., Jupyter notebooks,
|
|
67
|
+
async functions). Use input_async() instead in those environments.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
prompt: Task/prompt to send
|
|
71
|
+
timeout: Seconds to wait for response (default 30)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Agent's response string
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
RuntimeError: If called from within a running event loop
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> translator = connect("0x3d40...")
|
|
81
|
+
>>> result = translator.input("Translate 'hello' to Spanish")
|
|
82
|
+
>>> # Continue conversation
|
|
83
|
+
>>> result2 = translator.input("Now translate it to French")
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
asyncio.get_running_loop()
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
"input() cannot be used inside async context (e.g., Jupyter notebooks). "
|
|
89
|
+
"Use 'await agent.input_async()' instead."
|
|
90
|
+
)
|
|
91
|
+
except RuntimeError as e:
|
|
92
|
+
if "input() cannot be used" in str(e):
|
|
93
|
+
raise
|
|
94
|
+
# No running loop - safe to proceed
|
|
95
|
+
return asyncio.run(self._send_task(prompt, timeout))
|
|
96
|
+
|
|
97
|
+
async def input_async(self, prompt: str, timeout: float = 30.0) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Send task to remote agent and get response (async version).
|
|
100
|
+
|
|
101
|
+
Automatically maintains conversation context across calls.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
prompt: Task/prompt to send
|
|
105
|
+
timeout: Seconds to wait for response (default 30)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Agent's response string
|
|
109
|
+
"""
|
|
110
|
+
return await self._send_task(prompt, timeout)
|
|
111
|
+
|
|
112
|
+
def reset_conversation(self):
|
|
113
|
+
"""Clear conversation history and start fresh."""
|
|
114
|
+
self._session = None
|
|
115
|
+
|
|
116
|
+
def _sign_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
117
|
+
"""Sign a payload if keys are available."""
|
|
118
|
+
if not self._keys:
|
|
119
|
+
return {"prompt": payload.get("prompt", "")}
|
|
120
|
+
|
|
121
|
+
canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
|
|
122
|
+
signature = addr.sign(self._keys, canonical.encode())
|
|
123
|
+
return {
|
|
124
|
+
"payload": payload,
|
|
125
|
+
"from": self._keys["address"],
|
|
126
|
+
"signature": signature.hex()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async def _discover_endpoints(self) -> List[str]:
|
|
130
|
+
"""Query relay API for agent endpoints."""
|
|
131
|
+
import aiohttp
|
|
132
|
+
|
|
133
|
+
# Convert wss://oo.openonion.ai/ws/announce to https://oo.openonion.ai
|
|
134
|
+
base_url = self._relay_url.replace("wss://", "https://").replace("ws://", "http://")
|
|
135
|
+
base_url = base_url.replace("/ws/announce", "")
|
|
136
|
+
|
|
137
|
+
async with aiohttp.ClientSession() as session:
|
|
138
|
+
async with session.get(f"{base_url}/api/relay/agents/{self.address}") as resp:
|
|
139
|
+
if resp.status == 200:
|
|
140
|
+
data = await resp.json()
|
|
141
|
+
if data.get("online"):
|
|
142
|
+
return data.get("endpoints", [])
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
def _create_signed_body(self, prompt: str) -> Dict[str, Any]:
|
|
146
|
+
"""Create signed request body for agent /input endpoint."""
|
|
147
|
+
payload = {"prompt": prompt, "to": self.address, "timestamp": int(time.time())}
|
|
148
|
+
body = self._sign_payload(payload)
|
|
149
|
+
if self._session:
|
|
150
|
+
body["session"] = self._session
|
|
151
|
+
return body
|
|
152
|
+
|
|
153
|
+
async def _send_http(self, endpoint: str, prompt: str, timeout: float) -> str:
|
|
154
|
+
"""Send request via direct HTTP POST to agent /input endpoint."""
|
|
155
|
+
import aiohttp
|
|
156
|
+
|
|
157
|
+
body = self._create_signed_body(prompt)
|
|
158
|
+
|
|
159
|
+
async with aiohttp.ClientSession() as http_session:
|
|
160
|
+
async with http_session.post(
|
|
161
|
+
f"{endpoint}/input",
|
|
162
|
+
json=body,
|
|
163
|
+
timeout=aiohttp.ClientTimeout(total=timeout)
|
|
164
|
+
) as resp:
|
|
165
|
+
data = await resp.json()
|
|
166
|
+
if not resp.ok:
|
|
167
|
+
raise ConnectionError(data.get("error", f"HTTP {resp.status}"))
|
|
168
|
+
# Save session for conversation continuation
|
|
169
|
+
if "session" in data:
|
|
170
|
+
self._session = data["session"]
|
|
171
|
+
return data.get("result", "")
|
|
172
|
+
|
|
173
|
+
async def _send_relay(self, prompt: str, timeout: float) -> str:
|
|
174
|
+
"""Send request via WebSocket relay."""
|
|
175
|
+
import websockets
|
|
176
|
+
|
|
177
|
+
input_id = str(uuid.uuid4())
|
|
178
|
+
relay_input_url = self._relay_url.replace("/ws/announce", "/ws/input")
|
|
179
|
+
|
|
180
|
+
async with websockets.connect(relay_input_url) as ws:
|
|
181
|
+
payload = {"prompt": prompt, "to": self.address, "timestamp": int(time.time())}
|
|
182
|
+
signed = self._sign_payload(payload)
|
|
183
|
+
|
|
184
|
+
input_message = {
|
|
185
|
+
"type": "INPUT",
|
|
186
|
+
"input_id": input_id,
|
|
187
|
+
"to": self.address,
|
|
188
|
+
**signed
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await ws.send(json.dumps(input_message))
|
|
192
|
+
|
|
193
|
+
response_data = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
|
194
|
+
response = json.loads(response_data)
|
|
195
|
+
|
|
196
|
+
if response.get("type") == "OUTPUT" and response.get("input_id") == input_id:
|
|
197
|
+
return response.get("result", "")
|
|
198
|
+
elif response.get("type") == "ERROR":
|
|
199
|
+
raise ConnectionError(f"Agent error: {response.get('error')}")
|
|
200
|
+
else:
|
|
201
|
+
raise ConnectionError(f"Unexpected response: {response}")
|
|
202
|
+
|
|
203
|
+
async def _send_task(self, prompt: str, timeout: float) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Send task using best available connection method.
|
|
206
|
+
|
|
207
|
+
Priority:
|
|
208
|
+
1. Cached endpoint (if previously successful)
|
|
209
|
+
2. Discovered HTTPS endpoints
|
|
210
|
+
3. Discovered HTTP endpoints
|
|
211
|
+
4. Relay fallback
|
|
212
|
+
"""
|
|
213
|
+
# Try cached endpoint first
|
|
214
|
+
if self._cached_endpoint:
|
|
215
|
+
try:
|
|
216
|
+
return await self._send_http(self._cached_endpoint, prompt, timeout)
|
|
217
|
+
except Exception:
|
|
218
|
+
self._cached_endpoint = None # Clear failed cache
|
|
219
|
+
|
|
220
|
+
# Discover endpoints
|
|
221
|
+
endpoints = await self._discover_endpoints()
|
|
222
|
+
|
|
223
|
+
# Sort: HTTPS first, then HTTP
|
|
224
|
+
endpoints.sort(key=lambda e: (0 if e.startswith("https://") else 1))
|
|
225
|
+
|
|
226
|
+
# Try each endpoint
|
|
227
|
+
for endpoint in endpoints:
|
|
228
|
+
try:
|
|
229
|
+
result = await self._send_http(endpoint, prompt, timeout)
|
|
230
|
+
self._cached_endpoint = endpoint # Cache successful endpoint
|
|
231
|
+
return result
|
|
232
|
+
except Exception:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Fallback to relay
|
|
236
|
+
return await self._send_relay(prompt, timeout)
|
|
237
|
+
|
|
238
|
+
def __repr__(self):
|
|
239
|
+
short = self.address[:12] + "..." if len(self.address) > 12 else self.address
|
|
240
|
+
return f"RemoteAgent({short})"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def connect(
|
|
244
|
+
address: str,
|
|
245
|
+
*,
|
|
246
|
+
keys: Optional[Dict[str, Any]] = None,
|
|
247
|
+
relay_url: str = "wss://oo.openonion.ai/ws/announce"
|
|
248
|
+
) -> RemoteAgent:
|
|
249
|
+
"""
|
|
250
|
+
Connect to a remote agent.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
address: Agent's public key address (0x...)
|
|
254
|
+
keys: Signing keys from address.load() - required for strict trust agents
|
|
255
|
+
relay_url: Relay server URL (default: production)
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
RemoteAgent interface
|
|
259
|
+
|
|
260
|
+
Example:
|
|
261
|
+
>>> from connectonion import connect, address
|
|
262
|
+
>>>
|
|
263
|
+
>>> # Simple (unsigned)
|
|
264
|
+
>>> agent = connect("0x3d4017c3...")
|
|
265
|
+
>>> result = agent.input("Hello")
|
|
266
|
+
>>>
|
|
267
|
+
>>> # With signing (for strict trust agents)
|
|
268
|
+
>>> keys = address.load(Path(".co"))
|
|
269
|
+
>>> agent = connect("0x3d4017c3...", keys=keys)
|
|
270
|
+
>>> result = agent.input("Hello")
|
|
271
|
+
"""
|
|
272
|
+
return RemoteAgent(address, keys=keys, relay_url=relay_url)
|
|
@@ -9,7 +9,13 @@ Trust parameter accepts three forms:
|
|
|
9
9
|
3. Agent: Custom Agent instance for verification
|
|
10
10
|
|
|
11
11
|
All forms create a trust agent behind the scenes.
|
|
12
|
+
|
|
13
|
+
Worker Isolation:
|
|
14
|
+
Each request gets a fresh deep copy of the agent template.
|
|
15
|
+
This ensures complete isolation - tools with state (like BrowserTool)
|
|
16
|
+
don't interfere between concurrent requests.
|
|
12
17
|
"""
|
|
18
|
+
import copy
|
|
13
19
|
import hashlib
|
|
14
20
|
import json
|
|
15
21
|
import logging
|
|
@@ -102,17 +108,18 @@ class SessionStorage:
|
|
|
102
108
|
|
|
103
109
|
# === Handlers (pure functions) ===
|
|
104
110
|
|
|
105
|
-
def input_handler(
|
|
111
|
+
def input_handler(agent_template, storage: SessionStorage, prompt: str, result_ttl: int,
|
|
106
112
|
session: dict | None = None) -> dict:
|
|
107
113
|
"""POST /input
|
|
108
114
|
|
|
109
115
|
Args:
|
|
110
|
-
|
|
116
|
+
agent_template: The agent template (deep copied per request for isolation)
|
|
111
117
|
storage: SessionStorage for persisting results
|
|
112
118
|
prompt: The user's prompt
|
|
113
119
|
result_ttl: How long to keep the result on server
|
|
114
120
|
session: Optional conversation session for continuation
|
|
115
121
|
"""
|
|
122
|
+
agent = copy.deepcopy(agent_template)
|
|
116
123
|
now = time.time()
|
|
117
124
|
|
|
118
125
|
# Get or generate session_id
|
|
@@ -368,7 +375,12 @@ def admin_logs_handler(agent_name: str) -> dict:
|
|
|
368
375
|
|
|
369
376
|
|
|
370
377
|
def admin_sessions_handler() -> dict:
|
|
371
|
-
"""GET /admin/sessions - return
|
|
378
|
+
"""GET /admin/sessions - return raw session YAML files as JSON.
|
|
379
|
+
|
|
380
|
+
Returns session files as-is (converted from YAML to JSON). Each session
|
|
381
|
+
contains: name, created, updated, total_cost, total_tokens, turns array.
|
|
382
|
+
Frontend handles the display logic.
|
|
383
|
+
"""
|
|
372
384
|
import yaml
|
|
373
385
|
sessions_dir = Path(".co/sessions")
|
|
374
386
|
if not sessions_dir.exists():
|
|
@@ -381,30 +393,34 @@ def admin_sessions_handler() -> dict:
|
|
|
381
393
|
if session_data:
|
|
382
394
|
sessions.append(session_data)
|
|
383
395
|
|
|
384
|
-
# Sort by
|
|
385
|
-
sessions.sort(key=lambda s: s.get("created", ""), reverse=True)
|
|
396
|
+
# Sort by updated date descending (newest first)
|
|
397
|
+
sessions.sort(key=lambda s: s.get("updated", s.get("created", "")), reverse=True)
|
|
386
398
|
return {"sessions": sessions}
|
|
387
399
|
|
|
388
400
|
|
|
389
401
|
# === Entry Point ===
|
|
390
402
|
|
|
391
|
-
def _create_handlers(
|
|
403
|
+
def _create_handlers(agent_template, result_ttl: int):
|
|
392
404
|
"""Create handler dict for ASGI app."""
|
|
405
|
+
def ws_input(prompt: str) -> str:
|
|
406
|
+
agent = copy.deepcopy(agent_template)
|
|
407
|
+
return agent.input(prompt)
|
|
408
|
+
|
|
393
409
|
return {
|
|
394
|
-
"input": lambda storage, prompt, ttl, session=None: input_handler(
|
|
410
|
+
"input": lambda storage, prompt, ttl, session=None: input_handler(agent_template, storage, prompt, ttl, session),
|
|
395
411
|
"session": session_handler,
|
|
396
412
|
"sessions": sessions_handler,
|
|
397
|
-
"health": lambda start_time: health_handler(
|
|
398
|
-
"info": lambda trust: info_handler(
|
|
413
|
+
"health": lambda start_time: health_handler(agent_template, start_time),
|
|
414
|
+
"info": lambda trust: info_handler(agent_template, trust),
|
|
399
415
|
"auth": extract_and_authenticate,
|
|
400
|
-
"ws_input":
|
|
416
|
+
"ws_input": ws_input,
|
|
401
417
|
# Admin endpoints (auth required via OPENONION_API_KEY)
|
|
402
|
-
"admin_logs": lambda: admin_logs_handler(
|
|
418
|
+
"admin_logs": lambda: admin_logs_handler(agent_template.name),
|
|
403
419
|
"admin_sessions": admin_sessions_handler,
|
|
404
420
|
}
|
|
405
421
|
|
|
406
422
|
|
|
407
|
-
def _start_relay_background(
|
|
423
|
+
def _start_relay_background(agent_template, relay_url: str, addr_data: dict):
|
|
408
424
|
"""Start relay connection in background thread.
|
|
409
425
|
|
|
410
426
|
The relay connection runs alongside the HTTP server, allowing the agent
|
|
@@ -415,11 +431,12 @@ def _start_relay_background(agent, relay_url: str, addr_data: dict):
|
|
|
415
431
|
from . import announce, relay
|
|
416
432
|
|
|
417
433
|
# Create ANNOUNCE message
|
|
418
|
-
summary =
|
|
434
|
+
summary = agent_template.system_prompt[:1000] if agent_template.system_prompt else f"{agent_template.name} agent"
|
|
419
435
|
announce_msg = announce.create_announce_message(addr_data, summary, endpoints=[])
|
|
420
436
|
|
|
421
|
-
# Task handler
|
|
437
|
+
# Task handler - deep copy for each request
|
|
422
438
|
async def task_handler(prompt: str) -> str:
|
|
439
|
+
agent = copy.deepcopy(agent_template)
|
|
423
440
|
return agent.input(prompt)
|
|
424
441
|
|
|
425
442
|
async def relay_loop():
|