connectonion 0.5.8__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  """ConnectOnion - A simple agent framework with behavior tracking."""
2
2
 
3
- __version__ = "0.5.8"
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 using INPUT/OUTPUT protocol
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, tests/integration/manual/network_connect_manual.py]
5
- Data flow: user calls connect(address, relay_url) → creates RemoteAgent instance user calls .input(prompt) → _send_task() creates WebSocket to relay /ws/inputsends INPUT message with {type, input_id, to, prompt} → waits for OUTPUT response from relay → returns result string OR raises ConnectionError
6
- State/Effects: establishes temporary WebSocket connection per task (no persistent connection) | sends INPUT messages to relay | receives OUTPUT/ERROR messages | no file I/O or global state | asyncio.run() blocks on .input(), await on .input_async()
7
- Integration: exposes connect(address, relay_url), RemoteAgent class with .input(prompt, timeout), .input_async(prompt, timeout) | default relay_url="wss://oo.openonion.ai/ws/announce" | address format: 0x + 64 hex chars (Ed25519 public key) | complements host() with relay_url which listens for INPUT on relay | Protocol: INPUT type with to/prompt fields → OUTPUT type with input_id/result fields
8
- Performance: creates new WebSocket connection per input() call (no connection pooling) | default timeout=30s | async under the hood (asyncio.run wraps for sync API) | no caching or retry logic
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 firstfallback 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
- Simple function-based API for using remote agents.
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
- Minimal MVP: Just input() method.
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__(self, address: str, relay_url: str):
29
- self.address = address
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
- Use this in normal synchronous code.
37
- For async code, use input_async() instead.
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
- Use this when calling from async code.
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
- async def _send_task(self, prompt: str, timeout: float) -> str:
72
- """
73
- Send input via relay and wait for output.
112
+ def reset_conversation(self):
113
+ """Clear conversation history and start fresh."""
114
+ self._session = None
74
115
 
75
- MVP: Uses relay to route INPUT/OUTPUT messages between agents.
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
- # Send INPUT message
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
- "prompt": prompt
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(address: str, relay_url: str = "wss://oo.openonion.ai/ws/announce") -> RemoteAgent:
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
- >>> translator = connect("0x3d4017c3...")
126
- >>> result = translator.input("Translate 'hello' to Spanish")
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 all activity sessions as JSON array."""
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 created date descending (newest first)
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
 
@@ -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.8
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=pizZ0Tuzkb8z26SqkbZGfK9PpCTaytBdL_WL2TWhiFk,1887
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=dn5dWDkZZpKGW5St-CtAAfTM2-1IOe3luv0vQSA2ogk,5311
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=G3b50iuDv8BTOtL-6COOt9KcFVczzl-mROgQQVFFFfk,18690
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=gV-xAomC-UgA3VG7nUrDg_8AS8XZBxxgNLGL_Yxxzag,6232
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
@@ -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.8.dist-info/METADATA,sha256=u1GuMFftzTCEkj_N_cIfWNv2iEXHSsyHK-ggqin89qY,21902
111
- connectonion-0.5.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
112
- connectonion-0.5.8.dist-info/entry_points.txt,sha256=XDB-kVN7Qgy4DmYTkjQB_O6hZeUND-SqmZbdoQPn6WA,90
113
- connectonion-0.5.8.dist-info/RECORD,,
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,,