connectonion 0.5.7__py3-none-any.whl → 0.5.9__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.
- connectonion/__init__.py +3 -1
- connectonion/cli/commands/copy_commands.py +116 -0
- connectonion/cli/main.py +14 -1
- connectonion/connect.py +178 -34
- connectonion/host.py +8 -3
- connectonion/static/docs.html +97 -0
- connectonion/transcribe.py +245 -0
- {connectonion-0.5.7.dist-info → connectonion-0.5.9.dist-info}/METADATA +1 -1
- {connectonion-0.5.7.dist-info → connectonion-0.5.9.dist-info}/RECORD +11 -9
- {connectonion-0.5.7.dist-info → connectonion-0.5.9.dist-info}/WHEEL +0 -0
- {connectonion-0.5.7.dist-info → connectonion-0.5.9.dist-info}/entry_points.txt +0 -0
connectonion/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""ConnectOnion - A simple agent framework with behavior tracking."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.5.
|
|
3
|
+
__version__ = "0.5.9"
|
|
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]")
|
connectonion/cli/main.py
CHANGED
|
@@ -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()
|
connectonion/connect.py
CHANGED
|
@@ -1,40 +1,70 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Purpose: Client interface for connecting to remote agents via relay network
|
|
2
|
+
Purpose: Client interface for connecting to remote agents via HTTP or relay network
|
|
3
3
|
LLM-Note:
|
|
4
|
-
Dependencies: imports from [asyncio, json, uuid, websockets] | imported by [__init__.py, tests/test_connect.py, examples/] | tested by [tests/test_connect.py
|
|
5
|
-
Data flow:
|
|
6
|
-
State/Effects:
|
|
7
|
-
Integration: exposes connect(address, relay_url), RemoteAgent class with .input(
|
|
8
|
-
Performance:
|
|
9
|
-
Errors: raises ImportError if websockets not installed | raises ConnectionError for ERROR responses from relay | raises ConnectionError for unexpected response types | asyncio.TimeoutError if no response within timeout | WebSocket connection errors bubble up
|
|
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
|
|
10
9
|
|
|
11
10
|
Connect to remote agents on the network.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
Smart discovery: tries HTTP endpoints first, falls back to relay.
|
|
13
|
+
Always signs requests when keys are provided.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
17
|
import json
|
|
18
|
+
import time
|
|
18
19
|
import uuid
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from . import address as addr
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
class RemoteAgent:
|
|
22
26
|
"""
|
|
23
27
|
Interface to a remote agent.
|
|
24
28
|
|
|
25
|
-
|
|
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")
|
|
26
44
|
"""
|
|
27
45
|
|
|
28
|
-
def __init__(
|
|
29
|
-
self
|
|
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
|
|
30
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
|
|
31
58
|
|
|
32
59
|
def input(self, prompt: str, timeout: float = 30.0) -> str:
|
|
33
60
|
"""
|
|
34
61
|
Send task to remote agent and get response (sync version).
|
|
35
62
|
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
38
68
|
|
|
39
69
|
Args:
|
|
40
70
|
prompt: Task/prompt to send
|
|
@@ -43,17 +73,32 @@ class RemoteAgent:
|
|
|
43
73
|
Returns:
|
|
44
74
|
Agent's response string
|
|
45
75
|
|
|
76
|
+
Raises:
|
|
77
|
+
RuntimeError: If called from within a running event loop
|
|
78
|
+
|
|
46
79
|
Example:
|
|
47
80
|
>>> translator = connect("0x3d40...")
|
|
48
81
|
>>> result = translator.input("Translate 'hello' to Spanish")
|
|
82
|
+
>>> # Continue conversation
|
|
83
|
+
>>> result2 = translator.input("Now translate it to French")
|
|
49
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
|
|
50
95
|
return asyncio.run(self._send_task(prompt, timeout))
|
|
51
96
|
|
|
52
97
|
async def input_async(self, prompt: str, timeout: float = 30.0) -> str:
|
|
53
98
|
"""
|
|
54
99
|
Send task to remote agent and get response (async version).
|
|
55
100
|
|
|
56
|
-
|
|
101
|
+
Automatically maintains conversation context across calls.
|
|
57
102
|
|
|
58
103
|
Args:
|
|
59
104
|
prompt: Task/prompt to send
|
|
@@ -61,42 +106,93 @@ class RemoteAgent:
|
|
|
61
106
|
|
|
62
107
|
Returns:
|
|
63
108
|
Agent's response string
|
|
64
|
-
|
|
65
|
-
Example:
|
|
66
|
-
>>> remote = connect("0x3d40...")
|
|
67
|
-
>>> result = await remote.input_async("Translate 'hello' to Spanish")
|
|
68
109
|
"""
|
|
69
110
|
return await self._send_task(prompt, timeout)
|
|
70
111
|
|
|
71
|
-
|
|
72
|
-
"""
|
|
73
|
-
|
|
112
|
+
def reset_conversation(self):
|
|
113
|
+
"""Clear conversation history and start fresh."""
|
|
114
|
+
self._session = None
|
|
74
115
|
|
|
75
|
-
|
|
76
|
-
"""
|
|
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."""
|
|
77
175
|
import websockets
|
|
78
176
|
|
|
79
177
|
input_id = str(uuid.uuid4())
|
|
80
|
-
|
|
81
|
-
# Connect to relay input endpoint
|
|
82
178
|
relay_input_url = self._relay_url.replace("/ws/announce", "/ws/input")
|
|
83
179
|
|
|
84
180
|
async with websockets.connect(relay_input_url) as ws:
|
|
85
|
-
|
|
181
|
+
payload = {"prompt": prompt, "to": self.address, "timestamp": int(time.time())}
|
|
182
|
+
signed = self._sign_payload(payload)
|
|
183
|
+
|
|
86
184
|
input_message = {
|
|
87
185
|
"type": "INPUT",
|
|
88
186
|
"input_id": input_id,
|
|
89
187
|
"to": self.address,
|
|
90
|
-
|
|
188
|
+
**signed
|
|
91
189
|
}
|
|
92
190
|
|
|
93
191
|
await ws.send(json.dumps(input_message))
|
|
94
192
|
|
|
95
|
-
# Wait for OUTPUT
|
|
96
193
|
response_data = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
|
97
194
|
response = json.loads(response_data)
|
|
98
195
|
|
|
99
|
-
# Return result
|
|
100
196
|
if response.get("type") == "OUTPUT" and response.get("input_id") == input_id:
|
|
101
197
|
return response.get("result", "")
|
|
102
198
|
elif response.get("type") == "ERROR":
|
|
@@ -104,25 +200,73 @@ class RemoteAgent:
|
|
|
104
200
|
else:
|
|
105
201
|
raise ConnectionError(f"Unexpected response: {response}")
|
|
106
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
|
+
|
|
107
238
|
def __repr__(self):
|
|
108
239
|
short = self.address[:12] + "..." if len(self.address) > 12 else self.address
|
|
109
240
|
return f"RemoteAgent({short})"
|
|
110
241
|
|
|
111
242
|
|
|
112
|
-
def connect(
|
|
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:
|
|
113
249
|
"""
|
|
114
250
|
Connect to a remote agent.
|
|
115
251
|
|
|
116
252
|
Args:
|
|
117
253
|
address: Agent's public key address (0x...)
|
|
254
|
+
keys: Signing keys from address.load() - required for strict trust agents
|
|
118
255
|
relay_url: Relay server URL (default: production)
|
|
119
256
|
|
|
120
257
|
Returns:
|
|
121
258
|
RemoteAgent interface
|
|
122
259
|
|
|
123
260
|
Example:
|
|
124
|
-
>>> from connectonion import connect
|
|
125
|
-
>>>
|
|
126
|
-
>>>
|
|
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")
|
|
127
271
|
"""
|
|
128
|
-
return RemoteAgent(address, relay_url)
|
|
272
|
+
return RemoteAgent(address, keys=keys, relay_url=relay_url)
|
connectonion/host.py
CHANGED
|
@@ -368,7 +368,12 @@ def admin_logs_handler(agent_name: str) -> dict:
|
|
|
368
368
|
|
|
369
369
|
|
|
370
370
|
def admin_sessions_handler() -> dict:
|
|
371
|
-
"""GET /admin/sessions - return
|
|
371
|
+
"""GET /admin/sessions - return raw session YAML files as JSON.
|
|
372
|
+
|
|
373
|
+
Returns session files as-is (converted from YAML to JSON). Each session
|
|
374
|
+
contains: name, created, updated, total_cost, total_tokens, turns array.
|
|
375
|
+
Frontend handles the display logic.
|
|
376
|
+
"""
|
|
372
377
|
import yaml
|
|
373
378
|
sessions_dir = Path(".co/sessions")
|
|
374
379
|
if not sessions_dir.exists():
|
|
@@ -381,8 +386,8 @@ def admin_sessions_handler() -> dict:
|
|
|
381
386
|
if session_data:
|
|
382
387
|
sessions.append(session_data)
|
|
383
388
|
|
|
384
|
-
# Sort by
|
|
385
|
-
sessions.sort(key=lambda s: s.get("created", ""), reverse=True)
|
|
389
|
+
# Sort by updated date descending (newest first)
|
|
390
|
+
sessions.sort(key=lambda s: s.get("updated", s.get("created", "")), reverse=True)
|
|
386
391
|
return {"sessions": sessions}
|
|
387
392
|
|
|
388
393
|
|
connectonion/static/docs.html
CHANGED
|
@@ -366,6 +366,70 @@
|
|
|
366
366
|
</div>
|
|
367
367
|
</details>
|
|
368
368
|
|
|
369
|
+
<!-- Admin Endpoints Header -->
|
|
370
|
+
<div style="margin:24px 0 12px;padding:12px 16px;background:var(--panel);border:1px solid var(--panel-border);border-radius:4px;">
|
|
371
|
+
<div class="row between">
|
|
372
|
+
<div>
|
|
373
|
+
<h3 style="margin:0;font-size:14px;">Admin Endpoints</h3>
|
|
374
|
+
<p style="margin:4px 0 0;font-size:12px;color:var(--muted);">Requires OPENONION_API_KEY authorization</p>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="row" style="gap:8px;">
|
|
377
|
+
<label for="api-key" style="font-size:12px;margin:0;">API Key:</label>
|
|
378
|
+
<input type="password" id="api-key" placeholder="Enter API key..." style="width:200px;height:36px;font-size:12px;" />
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<!-- GET /admin/logs -->
|
|
384
|
+
<details class="endpoint">
|
|
385
|
+
<summary class="endpoint-header">
|
|
386
|
+
<span class="method get">GET</span>
|
|
387
|
+
<span class="endpoint-path">/admin/logs</span>
|
|
388
|
+
<span class="endpoint-desc">Get agent activity logs (auth required)</span>
|
|
389
|
+
<span class="endpoint-expand">▾</span>
|
|
390
|
+
</summary>
|
|
391
|
+
<div class="endpoint-body">
|
|
392
|
+
<button class="btn execute" onclick="fetchAdminLogs()">Execute</button>
|
|
393
|
+
<div class="endpoint-section" style="margin-top:12px;">
|
|
394
|
+
<h4>Curl</h4>
|
|
395
|
+
<div class="curl-box">
|
|
396
|
+
<pre id="admin-logs-curl">curl -X GET "http://localhost:8000/admin/logs" \
|
|
397
|
+
-H "Authorization: Bearer YOUR_API_KEY"</pre>
|
|
398
|
+
<button class="copy-btn" onclick="copyCurl('admin-logs-curl')">Copy</button>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
<div class="endpoint-section">
|
|
402
|
+
<h4>Response</h4>
|
|
403
|
+
<pre id="admin-logs-result" style="max-height:400px;">{ }</pre>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</details>
|
|
407
|
+
|
|
408
|
+
<!-- GET /admin/sessions -->
|
|
409
|
+
<details class="endpoint">
|
|
410
|
+
<summary class="endpoint-header">
|
|
411
|
+
<span class="method get">GET</span>
|
|
412
|
+
<span class="endpoint-path">/admin/sessions</span>
|
|
413
|
+
<span class="endpoint-desc">Get all activity sessions (auth required)</span>
|
|
414
|
+
<span class="endpoint-expand">▾</span>
|
|
415
|
+
</summary>
|
|
416
|
+
<div class="endpoint-body">
|
|
417
|
+
<button class="btn execute" onclick="fetchAdminSessions()">Execute</button>
|
|
418
|
+
<div class="endpoint-section" style="margin-top:12px;">
|
|
419
|
+
<h4>Curl</h4>
|
|
420
|
+
<div class="curl-box">
|
|
421
|
+
<pre id="admin-sessions-curl">curl -X GET "http://localhost:8000/admin/sessions" \
|
|
422
|
+
-H "Authorization: Bearer YOUR_API_KEY"</pre>
|
|
423
|
+
<button class="copy-btn" onclick="copyCurl('admin-sessions-curl')">Copy</button>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
<div class="endpoint-section">
|
|
427
|
+
<h4>Response</h4>
|
|
428
|
+
<pre id="admin-sessions-result" style="max-height:400px;">{ }</pre>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</details>
|
|
432
|
+
|
|
369
433
|
<!-- WebSocket /ws -->
|
|
370
434
|
<details class="endpoint">
|
|
371
435
|
<summary class="endpoint-header">
|
|
@@ -580,6 +644,39 @@
|
|
|
580
644
|
loadInfo(); refreshSessions();
|
|
581
645
|
}
|
|
582
646
|
|
|
647
|
+
async function fetchAdminLogs(){
|
|
648
|
+
const apiKey = $('api-key').value.trim();
|
|
649
|
+
if (!apiKey) {
|
|
650
|
+
$('admin-logs-result').textContent = '{"error": "Please enter API key"}';
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
$('admin-logs-result').textContent = 'Loading...';
|
|
654
|
+
const res = await fetch(buildUrl('/admin/logs'), {
|
|
655
|
+
headers: { 'Authorization': 'Bearer ' + apiKey }
|
|
656
|
+
});
|
|
657
|
+
if (res.ok) {
|
|
658
|
+
const text = await res.text();
|
|
659
|
+
$('admin-logs-result').textContent = text || '(empty)';
|
|
660
|
+
} else {
|
|
661
|
+
const data = await res.json().catch(() => ({ error: res.statusText }));
|
|
662
|
+
$('admin-logs-result').textContent = JSON.stringify(data, null, 2);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function fetchAdminSessions(){
|
|
667
|
+
const apiKey = $('api-key').value.trim();
|
|
668
|
+
if (!apiKey) {
|
|
669
|
+
$('admin-sessions-result').textContent = '{"error": "Please enter API key"}';
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
$('admin-sessions-result').textContent = 'Loading...';
|
|
673
|
+
const res = await fetch(buildUrl('/admin/sessions'), {
|
|
674
|
+
headers: { 'Authorization': 'Bearer ' + apiKey }
|
|
675
|
+
});
|
|
676
|
+
const data = await res.json();
|
|
677
|
+
$('admin-sessions-result').textContent = JSON.stringify(data, null, 2);
|
|
678
|
+
}
|
|
679
|
+
|
|
583
680
|
$('prompt').addEventListener('input', updatePayloadPreview);
|
|
584
681
|
$('from').addEventListener('input', updateCurlCommands);
|
|
585
682
|
$('signature').addEventListener('input', updateCurlCommands);
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Audio transcription utility using Gemini API
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [os, base64, pathlib, mimetypes, google.generativeai] | imported by [user code] | tested by [tests/test_transcribe.py]
|
|
5
|
+
Data flow: transcribe(audio, prompt, model) → load audio file → encode base64 → call Gemini API → return text
|
|
6
|
+
State/Effects: reads audio files from disk | makes Gemini API request | no caching
|
|
7
|
+
Integration: exposes transcribe(audio, prompt, model, timestamps) | similar pattern to llm_do()
|
|
8
|
+
Performance: one API call per transcription | files < 20MB use inline, larger use File API
|
|
9
|
+
Errors: raises ValueError if audio file not found | FileNotFoundError for missing files
|
|
10
|
+
|
|
11
|
+
Audio transcription utility for ConnectOnion framework.
|
|
12
|
+
|
|
13
|
+
This module provides the `transcribe()` function - a simple interface for
|
|
14
|
+
converting audio files to text using Gemini's multimodal capabilities.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
>>> from connectonion import transcribe
|
|
18
|
+
|
|
19
|
+
# Simple transcription
|
|
20
|
+
>>> text = transcribe("meeting.mp3")
|
|
21
|
+
|
|
22
|
+
# With context hints (improve accuracy for domain-specific terms)
|
|
23
|
+
>>> text = transcribe("meeting.mp3", prompt="Technical AI discussion, speakers: Aaron, Lisa")
|
|
24
|
+
|
|
25
|
+
# Different model
|
|
26
|
+
>>> text = transcribe("meeting.mp3", model="co/gemini-2.5-flash")
|
|
27
|
+
|
|
28
|
+
Supported formats: WAV, MP3, AIFF, AAC, OGG, FLAC
|
|
29
|
+
Token cost: 32 tokens per second of audio (1 minute = 1,920 tokens)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import base64
|
|
34
|
+
import mimetypes
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Optional
|
|
37
|
+
import httpx
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# MIME type mapping for audio formats
|
|
41
|
+
AUDIO_MIME_TYPES = {
|
|
42
|
+
".wav": "audio/wav",
|
|
43
|
+
".mp3": "audio/mp3",
|
|
44
|
+
".aiff": "audio/aiff",
|
|
45
|
+
".aac": "audio/aac",
|
|
46
|
+
".ogg": "audio/ogg",
|
|
47
|
+
".flac": "audio/flac",
|
|
48
|
+
".m4a": "audio/mp4",
|
|
49
|
+
".webm": "audio/webm",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Maximum file size for inline audio (20MB)
|
|
53
|
+
MAX_INLINE_SIZE = 20 * 1024 * 1024
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_mime_type(file_path: Path) -> str:
|
|
57
|
+
"""Get MIME type for audio file."""
|
|
58
|
+
suffix = file_path.suffix.lower()
|
|
59
|
+
if suffix in AUDIO_MIME_TYPES:
|
|
60
|
+
return AUDIO_MIME_TYPES[suffix]
|
|
61
|
+
# Fallback to mimetypes library
|
|
62
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
63
|
+
return mime_type or "audio/mpeg"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_api_key(model: str) -> str:
|
|
67
|
+
"""Get API key based on model."""
|
|
68
|
+
if model.startswith("co/"):
|
|
69
|
+
# Use OpenOnion managed keys
|
|
70
|
+
api_key = os.getenv("OPENONION_API_KEY")
|
|
71
|
+
if not api_key:
|
|
72
|
+
# Try loading from config file
|
|
73
|
+
config_path = Path.home() / ".connectonion" / ".co" / "config.toml"
|
|
74
|
+
if config_path.exists():
|
|
75
|
+
import toml
|
|
76
|
+
config = toml.load(config_path)
|
|
77
|
+
api_key = config.get("auth", {}).get("jwt_token")
|
|
78
|
+
if not api_key:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"OpenOnion API key required for co/ models. "
|
|
81
|
+
"Run `co auth` to authenticate or set OPENONION_API_KEY."
|
|
82
|
+
)
|
|
83
|
+
return api_key
|
|
84
|
+
else:
|
|
85
|
+
# Use Gemini API key directly
|
|
86
|
+
api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
|
|
87
|
+
if not api_key:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"Gemini API key required. Set GEMINI_API_KEY environment variable."
|
|
90
|
+
)
|
|
91
|
+
return api_key
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def transcribe(
|
|
95
|
+
audio: str,
|
|
96
|
+
prompt: Optional[str] = None,
|
|
97
|
+
model: str = "co/gemini-3-flash-preview",
|
|
98
|
+
timestamps: bool = False,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Transcribe audio file to text using Gemini.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
audio: Path to audio file (WAV, MP3, AIFF, AAC, OGG, FLAC)
|
|
105
|
+
prompt: Optional context hints for better accuracy
|
|
106
|
+
(e.g., "Technical AI discussion, speakers: Aaron, Lisa")
|
|
107
|
+
model: Model to use (default: co/gemini-3-flash-preview)
|
|
108
|
+
timestamps: If True, include timestamps in output
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Transcribed text
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
>>> # Simple transcription
|
|
115
|
+
>>> text = transcribe("meeting.mp3")
|
|
116
|
+
|
|
117
|
+
>>> # With context hints
|
|
118
|
+
>>> text = transcribe("meeting.mp3", prompt="Fix: ConnectOnion, OpenOnion")
|
|
119
|
+
|
|
120
|
+
>>> # With timestamps
|
|
121
|
+
>>> text = transcribe("podcast.mp3", timestamps=True)
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
FileNotFoundError: If audio file doesn't exist
|
|
125
|
+
ValueError: If API key is missing or invalid audio format
|
|
126
|
+
"""
|
|
127
|
+
# Validate file exists
|
|
128
|
+
file_path = Path(audio)
|
|
129
|
+
if not file_path.exists():
|
|
130
|
+
raise FileNotFoundError(f"Audio file not found: {audio}")
|
|
131
|
+
|
|
132
|
+
# Get file info
|
|
133
|
+
file_size = file_path.stat().st_size
|
|
134
|
+
mime_type = _get_mime_type(file_path)
|
|
135
|
+
|
|
136
|
+
# Read and encode audio
|
|
137
|
+
with open(file_path, "rb") as f:
|
|
138
|
+
audio_bytes = f.read()
|
|
139
|
+
audio_base64 = base64.standard_b64encode(audio_bytes).decode("utf-8")
|
|
140
|
+
|
|
141
|
+
# Build prompt
|
|
142
|
+
if timestamps:
|
|
143
|
+
system_prompt = "Transcribe this audio with timestamps in [MM:SS] format."
|
|
144
|
+
else:
|
|
145
|
+
system_prompt = "Transcribe this audio accurately."
|
|
146
|
+
|
|
147
|
+
if prompt:
|
|
148
|
+
system_prompt += f" Context: {prompt}"
|
|
149
|
+
|
|
150
|
+
# Get API key and model name
|
|
151
|
+
api_key = _get_api_key(model)
|
|
152
|
+
actual_model = model[3:] if model.startswith("co/") else model
|
|
153
|
+
|
|
154
|
+
# Use OpenOnion proxy for co/ models, direct Gemini API otherwise
|
|
155
|
+
if model.startswith("co/"):
|
|
156
|
+
return _transcribe_via_openonion(
|
|
157
|
+
audio_base64, mime_type, system_prompt, api_key, actual_model
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
return _transcribe_via_gemini(
|
|
161
|
+
audio_base64, mime_type, system_prompt, api_key, actual_model
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _transcribe_via_gemini(
|
|
166
|
+
audio_base64: str,
|
|
167
|
+
mime_type: str,
|
|
168
|
+
prompt: str,
|
|
169
|
+
api_key: str,
|
|
170
|
+
model: str,
|
|
171
|
+
) -> str:
|
|
172
|
+
"""Transcribe using Gemini's OpenAI-compatible endpoint."""
|
|
173
|
+
import openai
|
|
174
|
+
|
|
175
|
+
client = openai.OpenAI(
|
|
176
|
+
api_key=api_key,
|
|
177
|
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
response = client.chat.completions.create(
|
|
181
|
+
model=model,
|
|
182
|
+
messages=[
|
|
183
|
+
{
|
|
184
|
+
"role": "user",
|
|
185
|
+
"content": [
|
|
186
|
+
{"type": "text", "text": prompt},
|
|
187
|
+
{
|
|
188
|
+
"type": "input_audio",
|
|
189
|
+
"input_audio": {
|
|
190
|
+
"data": audio_base64,
|
|
191
|
+
"format": mime_type.split("/")[-1], # e.g., "mp3"
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return response.choices[0].message.content
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _transcribe_via_openonion(
|
|
203
|
+
audio_base64: str,
|
|
204
|
+
mime_type: str,
|
|
205
|
+
prompt: str,
|
|
206
|
+
api_key: str,
|
|
207
|
+
model: str,
|
|
208
|
+
) -> str:
|
|
209
|
+
"""Transcribe using OpenOnion proxy (for co/ models)."""
|
|
210
|
+
# Determine API URL
|
|
211
|
+
is_dev = os.getenv("OPENONION_DEV") or os.getenv("ENVIRONMENT") == "development"
|
|
212
|
+
base_url = "http://localhost:8000" if is_dev else "https://oo.openonion.ai"
|
|
213
|
+
|
|
214
|
+
# Build request
|
|
215
|
+
request_body = {
|
|
216
|
+
"model": model,
|
|
217
|
+
"messages": [
|
|
218
|
+
{
|
|
219
|
+
"role": "user",
|
|
220
|
+
"content": [
|
|
221
|
+
{"type": "text", "text": prompt},
|
|
222
|
+
{
|
|
223
|
+
"type": "input_audio",
|
|
224
|
+
"input_audio": {
|
|
225
|
+
"data": audio_base64,
|
|
226
|
+
"format": mime_type.split("/")[-1],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
response = httpx.post(
|
|
235
|
+
f"{base_url}/v1/chat/completions",
|
|
236
|
+
json=request_body,
|
|
237
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
238
|
+
timeout=120.0,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if response.status_code != 200:
|
|
242
|
+
raise ValueError(f"Transcription failed: {response.status_code} - {response.text}")
|
|
243
|
+
|
|
244
|
+
data = response.json()
|
|
245
|
+
return data["choices"][0]["message"]["content"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: connectonion
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.9
|
|
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,15 +1,15 @@
|
|
|
1
|
-
connectonion/__init__.py,sha256=
|
|
1
|
+
connectonion/__init__.py,sha256=68MssCsfsorARt3OP7Bs-31RhUfhz-tUVHC5IJhkvfI,1940
|
|
2
2
|
connectonion/address.py,sha256=YOzpMOej-HqJUE6o0i0fG8rB7HM-Iods36s9OD--5ig,10852
|
|
3
3
|
connectonion/agent.py,sha256=BHnP4N0odXCSn9xT0QnJpXj3VV-E_vUtNXJ0M6k3RNs,18889
|
|
4
4
|
connectonion/announce.py,sha256=47Lxe8S4yyTbpsmYUmakU_DehrGvljyldmPfKnAOrFQ,3365
|
|
5
5
|
connectonion/asgi.py,sha256=VTMwwEWLq5RYvIufRImMcv-AB5D5pZmM8INaXqwUt4Q,9621
|
|
6
6
|
connectonion/auto_debug_exception.py,sha256=iA-b5GC40AI4YVXen2UCkDkfHLVZehFPgZrinIxykWI,8147
|
|
7
|
-
connectonion/connect.py,sha256=
|
|
7
|
+
connectonion/connect.py,sha256=dYuBKxUvcNeyR0IbcKrnspwylp7177JmBYz5DsRhiXQ,9794
|
|
8
8
|
connectonion/console.py,sha256=6_J1ItkLJCHDpdJY-tCuw4jMcS09S-a5juZSDSIr1Nc,21254
|
|
9
9
|
connectonion/debugger_ui.py,sha256=QMyoZkhGbt-pStHHjpnCBXtfzvfqPW94tXiL07KZiAw,41741
|
|
10
10
|
connectonion/decorators.py,sha256=YFmZMptcchIgNriKFf_vOyacor5i_j6Cy_igTJhdKm4,7141
|
|
11
11
|
connectonion/events.py,sha256=jJOMt1PhRl-ef4R8-WpAq8pUbZ8GKIl0wOB2kJUVyWg,9151
|
|
12
|
-
connectonion/host.py,sha256=
|
|
12
|
+
connectonion/host.py,sha256=ccCMbw2fkd0LaAnor4UuNmTzfa_agmAzYvwperShsuo,18902
|
|
13
13
|
connectonion/interactive_debugger.py,sha256=XHSCGJp9YV6VAZM1gk_AuxKAdBODJQUcLVWaLuTMqv0,16277
|
|
14
14
|
connectonion/llm.py,sha256=IzjlOT6Dj5xIplIymjcaHZ_abwE5wDHEE4pa8zjlEGY,32952
|
|
15
15
|
connectonion/llm_do.py,sha256=YI7kGFKpB6KUEG_CKtP3Bl4IWmRac7TCa9GK5FSV624,12000
|
|
@@ -19,6 +19,7 @@ connectonion/relay.py,sha256=a8wj4UZDZpGEhvpyDuRjZJg1S1VzxqiPioQRLq1EAao,7160
|
|
|
19
19
|
connectonion/tool_executor.py,sha256=IPCVRS20XOd5JsZrdXRwK-gI6Xb0BtDQ3fCD0qt2dcE,10617
|
|
20
20
|
connectonion/tool_factory.py,sha256=vo4_pyhqKG_OtfpjV15oPG4zlkxn40768PW5pQVtx-g,6704
|
|
21
21
|
connectonion/tool_registry.py,sha256=rTe8RJnWXpmHXWznP468fhWvmRknk-TlF9Iz8G9_qus,4367
|
|
22
|
+
connectonion/transcribe.py,sha256=m2qd7A2qMKFAbmbLLgvcd-yUFz8FHgjUKtvtFffnK00,7730
|
|
22
23
|
connectonion/trust.py,sha256=It4ueuMvCmHOD7FwaV-jSwlonWFsFgNH1gz9fJtmfW4,6692
|
|
23
24
|
connectonion/trust_agents.py,sha256=XDedEhxGRfu9KeYhX-Z1a5tRA-2Zbwz4ztnlA2Lnaf0,2968
|
|
24
25
|
connectonion/trust_functions.py,sha256=jRgfPm5z8oy9x8IYJd20UUMCz7cc1Pd7zyqQK5SDdLc,3564
|
|
@@ -26,13 +27,14 @@ connectonion/usage.py,sha256=mS_J5it9NMDI1CjycNUJEnXGNXAo1lQ1ICEZvqftTpU,6205
|
|
|
26
27
|
connectonion/xray.py,sha256=TA_VychqQRtfSzO3RmTqnDZZNx3LjycbgUWVyMswAIw,18542
|
|
27
28
|
connectonion/cli/__init__.py,sha256=Pd1iKp0rUghs2kmdAhyp9hK8j0uWkNxOagBkdCGrG4I,55
|
|
28
29
|
connectonion/cli/docs.md,sha256=Fk_JT8jFXXDpXTvU0ZUigMW1UR6ERmX-HDheYPPRNY8,3231
|
|
29
|
-
connectonion/cli/main.py,sha256=
|
|
30
|
+
connectonion/cli/main.py,sha256=2FWi-yhnyLQzZl_esOXYY3-u0Q1wbpdE4XG3QYoGm_Q,6893
|
|
30
31
|
connectonion/cli/browser_agent/__init__.py,sha256=xZCoxS3dGVBBMnaasHOE1vxMDdIwUloRP67rGR-4obo,173
|
|
31
32
|
connectonion/cli/browser_agent/browser.py,sha256=EdBAvpcfCHh8q6_-3-LVBw6OHyMYnz1_8m6gryWQdzM,8830
|
|
32
33
|
connectonion/cli/browser_agent/prompt.md,sha256=tF0PhGcl3gkmVK1F5SG9GZwnPPXTZYWEaatAZtOYZZg,4479
|
|
33
34
|
connectonion/cli/commands/__init__.py,sha256=IPZy9NwrE0hs4f7hIqyFM9GjFZpeCS8mC0Jf-5Ooy4c,43
|
|
34
35
|
connectonion/cli/commands/auth_commands.py,sha256=D76_0yd77d23bXRvsTAY6HOcGJswo9-6z2dRi9CR9sE,21635
|
|
35
36
|
connectonion/cli/commands/browser_commands.py,sha256=lB4N6XwP43qdcthwQWlbFU2S3INfxhRDXznAw1PSTsQ,1803
|
|
37
|
+
connectonion/cli/commands/copy_commands.py,sha256=9cRpTFlBKrFXgJw582YJqKLq6o1dmwPB_c9RHEJc76c,3852
|
|
36
38
|
connectonion/cli/commands/create.py,sha256=0TrsPulS6dqwr_gtIWWBQ03Q_BnNro-CV4qOV5VkHAg,22011
|
|
37
39
|
connectonion/cli/commands/deploy_commands.py,sha256=s0bjApUQbNlMGStUSrAB8RqSSewBlHrBL60NWgEIHhc,8248
|
|
38
40
|
connectonion/cli/commands/doctor_commands.py,sha256=EOk8CMclvVqLq4-Dg2JghWehH9VQnthBejrhIBX66_4,7244
|
|
@@ -73,7 +75,7 @@ connectonion/prompt_files/eval_expected.md,sha256=ZnVmyF9S9m49UzTKCMajJhfGaxUhUk
|
|
|
73
75
|
connectonion/prompt_files/react_evaluate.md,sha256=1qzq_ShlhWUQnh8PkdMl9IpHSwR67FguNc8vaLxzSkA,404
|
|
74
76
|
connectonion/prompt_files/react_plan.md,sha256=QwEDnHsCs1A9AGTyuA1puLRHT0MkUrNEAbVhF3yWNEc,492
|
|
75
77
|
connectonion/prompt_files/reflect.md,sha256=aMaWNbHCyxX3AtqyUrrJurAG6w45lcSo05RrAv6UiIM,627
|
|
76
|
-
connectonion/static/docs.html,sha256=
|
|
78
|
+
connectonion/static/docs.html,sha256=rSffkCAy2fA512XcMC6z1wyZH9iYeAsSP213G3l1OOk,31085
|
|
77
79
|
connectonion/tui/__init__.py,sha256=L_4oqFmghtcqD21QVe3hNMnDlDMkF8sXr59fG53xbG0,2232
|
|
78
80
|
connectonion/tui/divider.py,sha256=pKOj7Y3lPwB4TxoWs9J7Pi1axyzMELuw5TIARLyud0o,1018
|
|
79
81
|
connectonion/tui/dropdown.py,sha256=fhxfX2N4vjYvwVPbExcwE23lpgk55xTgmeKyFUIaOE8,8127
|
|
@@ -107,7 +109,7 @@ connectonion/useful_tools/slash_command.py,sha256=VKl7SKLyaxHcHwfE8rgzBCQ2-KQtL0
|
|
|
107
109
|
connectonion/useful_tools/terminal.py,sha256=PtzGHN6vnWROmssi37YOZ5U-d0Z7Fe79bocoKAngoxg,9330
|
|
108
110
|
connectonion/useful_tools/todo_list.py,sha256=wVEt4kt-MczIcP3xvKelfshGHZ6EATpDFzQx0zov8cw,7483
|
|
109
111
|
connectonion/useful_tools/web_fetch.py,sha256=CysJLOdDIkbMVJ12Dj3WgsytLu1-o2wrwNopqA_S1jk,7253
|
|
110
|
-
connectonion-0.5.
|
|
111
|
-
connectonion-0.5.
|
|
112
|
-
connectonion-0.5.
|
|
113
|
-
connectonion-0.5.
|
|
112
|
+
connectonion-0.5.9.dist-info/METADATA,sha256=oxWbyqFobBFwXBIB8m4-zAA0m1N3R_ZCb8xw_AznETM,21902
|
|
113
|
+
connectonion-0.5.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
114
|
+
connectonion-0.5.9.dist-info/entry_points.txt,sha256=XDB-kVN7Qgy4DmYTkjQB_O6hZeUND-SqmZbdoQPn6WA,90
|
|
115
|
+
connectonion-0.5.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|