connectonion 0.5.8__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 +78 -0
- connectonion/address.py +320 -0
- connectonion/agent.py +450 -0
- connectonion/announce.py +84 -0
- connectonion/asgi.py +287 -0
- connectonion/auto_debug_exception.py +181 -0
- connectonion/cli/__init__.py +3 -0
- connectonion/cli/browser_agent/__init__.py +5 -0
- connectonion/cli/browser_agent/browser.py +243 -0
- connectonion/cli/browser_agent/prompt.md +107 -0
- connectonion/cli/commands/__init__.py +1 -0
- connectonion/cli/commands/auth_commands.py +527 -0
- connectonion/cli/commands/browser_commands.py +27 -0
- connectonion/cli/commands/create.py +511 -0
- connectonion/cli/commands/deploy_commands.py +220 -0
- connectonion/cli/commands/doctor_commands.py +173 -0
- connectonion/cli/commands/init.py +469 -0
- connectonion/cli/commands/project_cmd_lib.py +828 -0
- connectonion/cli/commands/reset_commands.py +149 -0
- connectonion/cli/commands/status_commands.py +168 -0
- connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
- connectonion/cli/docs/connectonion.md +1256 -0
- connectonion/cli/docs.md +123 -0
- connectonion/cli/main.py +148 -0
- connectonion/cli/templates/meta-agent/README.md +287 -0
- connectonion/cli/templates/meta-agent/agent.py +196 -0
- connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
- connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
- connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
- connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
- connectonion/cli/templates/minimal/README.md +56 -0
- connectonion/cli/templates/minimal/agent.py +40 -0
- connectonion/cli/templates/playwright/README.md +118 -0
- connectonion/cli/templates/playwright/agent.py +336 -0
- connectonion/cli/templates/playwright/prompt.md +102 -0
- connectonion/cli/templates/playwright/requirements.txt +3 -0
- connectonion/cli/templates/web-research/agent.py +122 -0
- connectonion/connect.py +128 -0
- connectonion/console.py +539 -0
- connectonion/debug_agent/__init__.py +13 -0
- connectonion/debug_agent/agent.py +45 -0
- connectonion/debug_agent/prompts/debug_assistant.md +72 -0
- connectonion/debug_agent/runtime_inspector.py +406 -0
- connectonion/debug_explainer/__init__.py +10 -0
- connectonion/debug_explainer/explain_agent.py +114 -0
- connectonion/debug_explainer/explain_context.py +263 -0
- connectonion/debug_explainer/explainer_prompt.md +29 -0
- connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
- connectonion/debugger_ui.py +1039 -0
- connectonion/decorators.py +208 -0
- connectonion/events.py +248 -0
- connectonion/execution_analyzer/__init__.py +9 -0
- connectonion/execution_analyzer/execution_analysis.py +93 -0
- connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
- connectonion/host.py +579 -0
- connectonion/interactive_debugger.py +342 -0
- connectonion/llm.py +801 -0
- connectonion/llm_do.py +307 -0
- connectonion/logger.py +300 -0
- connectonion/prompt_files/__init__.py +1 -0
- connectonion/prompt_files/analyze_contact.md +62 -0
- connectonion/prompt_files/eval_expected.md +12 -0
- connectonion/prompt_files/react_evaluate.md +11 -0
- connectonion/prompt_files/react_plan.md +16 -0
- connectonion/prompt_files/reflect.md +22 -0
- connectonion/prompts.py +144 -0
- connectonion/relay.py +200 -0
- connectonion/static/docs.html +688 -0
- connectonion/tool_executor.py +279 -0
- connectonion/tool_factory.py +186 -0
- connectonion/tool_registry.py +105 -0
- connectonion/trust.py +166 -0
- connectonion/trust_agents.py +71 -0
- connectonion/trust_functions.py +88 -0
- connectonion/tui/__init__.py +57 -0
- connectonion/tui/divider.py +39 -0
- connectonion/tui/dropdown.py +251 -0
- connectonion/tui/footer.py +31 -0
- connectonion/tui/fuzzy.py +56 -0
- connectonion/tui/input.py +278 -0
- connectonion/tui/keys.py +35 -0
- connectonion/tui/pick.py +130 -0
- connectonion/tui/providers.py +155 -0
- connectonion/tui/status_bar.py +163 -0
- connectonion/usage.py +161 -0
- connectonion/useful_events_handlers/__init__.py +16 -0
- connectonion/useful_events_handlers/reflect.py +116 -0
- connectonion/useful_plugins/__init__.py +20 -0
- connectonion/useful_plugins/calendar_plugin.py +163 -0
- connectonion/useful_plugins/eval.py +139 -0
- connectonion/useful_plugins/gmail_plugin.py +162 -0
- connectonion/useful_plugins/image_result_formatter.py +127 -0
- connectonion/useful_plugins/re_act.py +78 -0
- connectonion/useful_plugins/shell_approval.py +159 -0
- connectonion/useful_tools/__init__.py +44 -0
- connectonion/useful_tools/diff_writer.py +192 -0
- connectonion/useful_tools/get_emails.py +183 -0
- connectonion/useful_tools/gmail.py +1596 -0
- connectonion/useful_tools/google_calendar.py +613 -0
- connectonion/useful_tools/memory.py +380 -0
- connectonion/useful_tools/microsoft_calendar.py +604 -0
- connectonion/useful_tools/outlook.py +488 -0
- connectonion/useful_tools/send_email.py +205 -0
- connectonion/useful_tools/shell.py +97 -0
- connectonion/useful_tools/slash_command.py +201 -0
- connectonion/useful_tools/terminal.py +285 -0
- connectonion/useful_tools/todo_list.py +241 -0
- connectonion/useful_tools/web_fetch.py +216 -0
- connectonion/xray.py +467 -0
- connectonion-0.5.8.dist-info/METADATA +741 -0
- connectonion-0.5.8.dist-info/RECORD +113 -0
- connectonion-0.5.8.dist-info/WHEEL +4 -0
- connectonion-0.5.8.dist-info/entry_points.txt +3 -0
connectonion/llm_do.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: One-shot LLM function for simple single-round calls without agent overhead
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [typing, pathlib, pydantic, dotenv, prompts.py, llm.py] | imported by [debug_explainer/explain_context.py, user code, examples] | tested by [tests/test_llm_do.py, tests/test_llm_do_comprehensive.py, tests/test_real_llm_do.py]
|
|
5
|
+
Data flow: user calls llm_do(input, output, system_prompt, model, api_key, **kwargs) → validates input non-empty → loads system_prompt via load_system_prompt() → builds messages [system, user] → calls create_llm(model, api_key) factory → calls llm.complete(messages, **kwargs) OR llm.structured_complete(messages, output, **kwargs) → returns string OR Pydantic model instance
|
|
6
|
+
State/Effects: loads .env via dotenv.load_dotenv() | reads system_prompt files if Path provided | makes one LLM API request | no caching or persistence | stateless
|
|
7
|
+
Integration: exposes llm_do(input, output, system_prompt, model, api_key, **kwargs) | default model="co/gemini-2.5-flash" (managed keys) | default temperature=0.1 | supports all create_llm() providers | **kwargs pass through to provider (max_tokens, temperature, etc.)
|
|
8
|
+
Performance: minimal overhead (no agent loop, no tool calling, no conversation history) | one LLM call per invocation | no caching | synchronous blocking
|
|
9
|
+
Errors: raises ValueError if input empty | provider errors from create_llm() and llm.complete() bubble up | Pydantic ValidationError if structured output doesn't match schema
|
|
10
|
+
|
|
11
|
+
One-shot LLM function for simple, single-round calls with optional structured output.
|
|
12
|
+
|
|
13
|
+
This module provides the `llm_do()` function - a simplified interface for making
|
|
14
|
+
one-shot LLM calls without the overhead of the full Agent system. Perfect for
|
|
15
|
+
simple tasks that don't require multi-step reasoning or tool calling.
|
|
16
|
+
|
|
17
|
+
Purpose
|
|
18
|
+
-------
|
|
19
|
+
`llm_do()` is designed for:
|
|
20
|
+
- Quick LLM calls without agent overhead
|
|
21
|
+
- Data extraction with Pydantic validation
|
|
22
|
+
- Simple Q&A and text generation
|
|
23
|
+
- Format conversion (text → JSON, etc.)
|
|
24
|
+
- One-shot analysis tasks
|
|
25
|
+
|
|
26
|
+
NOT designed for:
|
|
27
|
+
- Multi-step workflows (use Agent instead)
|
|
28
|
+
- Tool calling (use Agent instead)
|
|
29
|
+
- Iterative refinement (use Agent instead)
|
|
30
|
+
- Maintaining conversation history (use Agent instead)
|
|
31
|
+
|
|
32
|
+
Architecture
|
|
33
|
+
-----------
|
|
34
|
+
The function is a thin wrapper around the LLM provider abstraction:
|
|
35
|
+
|
|
36
|
+
1. **Input Validation**: Ensures non-empty input
|
|
37
|
+
2. **System Prompt Loading**: Loads from string or file path
|
|
38
|
+
3. **Message Building**: Constructs OpenAI-format message list
|
|
39
|
+
4. **LLM Selection**: Uses create_llm() factory to get provider
|
|
40
|
+
5. **Response Handling**: Routes to complete() or structured_complete()
|
|
41
|
+
|
|
42
|
+
Key Design Decisions
|
|
43
|
+
-------------------
|
|
44
|
+
- **Stateless**: No conversation history, each call is independent
|
|
45
|
+
- **Simple API**: Minimal parameters, sensible defaults
|
|
46
|
+
- **Default Model**: Uses "co/gemini-2.5-flash" (ConnectOnion managed keys) for zero-setup
|
|
47
|
+
- **Structured Output**: Native Pydantic support via provider-specific APIs
|
|
48
|
+
- **Flexible Parameters**: **kwargs pass through to underlying LLM (temperature, max_tokens, etc.)
|
|
49
|
+
|
|
50
|
+
Comparison with Agent
|
|
51
|
+
--------------------
|
|
52
|
+
┌─────────────────┬──────────────┬─────────────────┐
|
|
53
|
+
│ Feature │ llm_do() │ Agent() │
|
|
54
|
+
├─────────────────┼──────────────┼─────────────────┤
|
|
55
|
+
│ Iterations │ Always 1 │ Up to max_iters │
|
|
56
|
+
│ Tools │ No │ Yes │
|
|
57
|
+
│ State │ Stateless │ Maintains hist │
|
|
58
|
+
│ Use case │ Quick tasks │ Complex flows │
|
|
59
|
+
│ Overhead │ Minimal │ Full framework │
|
|
60
|
+
└─────────────────┴──────────────┴─────────────────┘
|
|
61
|
+
|
|
62
|
+
Data Flow
|
|
63
|
+
---------
|
|
64
|
+
User code → llm_do(input, output, model, **kwargs)
|
|
65
|
+
↓
|
|
66
|
+
Validate input → Load system_prompt → Build messages
|
|
67
|
+
↓
|
|
68
|
+
create_llm(model, api_key) → Provider instance
|
|
69
|
+
↓
|
|
70
|
+
┌─────────────────────────────────────┐
|
|
71
|
+
│ If output (Pydantic model): │
|
|
72
|
+
│ provider.structured_complete() │
|
|
73
|
+
│ → Pydantic instance │
|
|
74
|
+
│ │
|
|
75
|
+
│ If no output: │
|
|
76
|
+
│ provider.complete() │
|
|
77
|
+
│ → String content │
|
|
78
|
+
└─────────────────────────────────────┘
|
|
79
|
+
↓
|
|
80
|
+
Return result to user
|
|
81
|
+
|
|
82
|
+
Supported Providers
|
|
83
|
+
------------------
|
|
84
|
+
All providers from llm.py module:
|
|
85
|
+
|
|
86
|
+
1. **OpenAI**: gpt-4o, gpt-4o-mini, gpt-3.5-turbo, o4-mini
|
|
87
|
+
- Native structured output via responses.parse()
|
|
88
|
+
- Fastest structured output implementation
|
|
89
|
+
|
|
90
|
+
2. **Anthropic**: claude-3-5-sonnet, claude-3-5-haiku-20241022
|
|
91
|
+
- Structured output via forced tool calling
|
|
92
|
+
- Requires max_tokens parameter (default: 8192)
|
|
93
|
+
|
|
94
|
+
3. **Google Gemini**: gemini-2.5-flash, gemini-2.5-pro
|
|
95
|
+
- Structured output via response_schema
|
|
96
|
+
- Good balance of speed and quality
|
|
97
|
+
|
|
98
|
+
4. **ConnectOnion**: co/gpt-4o, co/o4-mini (DEFAULT)
|
|
99
|
+
- Managed API keys (no env vars needed!)
|
|
100
|
+
- Proxies to OpenAI with usage tracking
|
|
101
|
+
- Requires: run `co auth` first
|
|
102
|
+
|
|
103
|
+
Usage Patterns
|
|
104
|
+
-------------
|
|
105
|
+
1. **Simple Q&A**:
|
|
106
|
+
>>> answer = llm_do("What is 2+2?")
|
|
107
|
+
>>> print(answer) # "4"
|
|
108
|
+
|
|
109
|
+
2. **Structured Extraction**:
|
|
110
|
+
>>> class Person(BaseModel):
|
|
111
|
+
... name: str
|
|
112
|
+
... age: int
|
|
113
|
+
>>> result = llm_do("John, 30 years old", output=Person)
|
|
114
|
+
>>> result.name # "John"
|
|
115
|
+
|
|
116
|
+
3. **Custom System Prompt**:
|
|
117
|
+
>>> answer = llm_do(
|
|
118
|
+
... "Hello",
|
|
119
|
+
... system_prompt="You are a pirate. Always respond like a pirate."
|
|
120
|
+
... )
|
|
121
|
+
|
|
122
|
+
4. **Different Provider**:
|
|
123
|
+
>>> answer = llm_do("Hello", model="claude-3-5-haiku-20241022")
|
|
124
|
+
|
|
125
|
+
5. **Runtime Parameters**:
|
|
126
|
+
>>> answer = llm_do(
|
|
127
|
+
... "Write a story",
|
|
128
|
+
... temperature=0.9, # More creative
|
|
129
|
+
... max_tokens=100 # Short response
|
|
130
|
+
... )
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
- input (str): The text/question to send to the LLM
|
|
135
|
+
- output (Type[BaseModel], optional): Pydantic model for structured output
|
|
136
|
+
- system_prompt (str | Path, optional): System instructions (inline or file path)
|
|
137
|
+
- model (str): Model name (default: "co/gemini-2.5-flash")
|
|
138
|
+
- temperature (float): Sampling temperature (default: 0.1 for consistency)
|
|
139
|
+
- api_key (str, optional): Override API key (uses env vars by default)
|
|
140
|
+
- **kwargs: Additional parameters passed to LLM (max_tokens, top_p, etc.)
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
- str: Plain text response (when output is None)
|
|
145
|
+
- BaseModel: Validated Pydantic instance (when output is provided)
|
|
146
|
+
|
|
147
|
+
Raises
|
|
148
|
+
------
|
|
149
|
+
- ValueError: If input is empty
|
|
150
|
+
- ValueError: If API key is missing
|
|
151
|
+
- ValueError: If model is unknown
|
|
152
|
+
- ValidationError: If structured output doesn't match schema
|
|
153
|
+
- Provider-specific errors: From underlying LLM SDKs
|
|
154
|
+
|
|
155
|
+
Environment Variables
|
|
156
|
+
--------------------
|
|
157
|
+
Optional (choose based on model):
|
|
158
|
+
- OPENAI_API_KEY: For OpenAI models
|
|
159
|
+
- ANTHROPIC_API_KEY: For Claude models
|
|
160
|
+
- GEMINI_API_KEY or GOOGLE_API_KEY: For Gemini models
|
|
161
|
+
- OPENONION_API_KEY: For co/ models (or run `co auth`)
|
|
162
|
+
|
|
163
|
+
Dependencies
|
|
164
|
+
-----------
|
|
165
|
+
- llm.py: create_llm() factory and provider implementations
|
|
166
|
+
- prompts.py: load_system_prompt() for file-based prompts
|
|
167
|
+
- pydantic: BaseModel validation for structured output
|
|
168
|
+
- dotenv: Loads .env file automatically
|
|
169
|
+
|
|
170
|
+
Integration Points
|
|
171
|
+
-----------------
|
|
172
|
+
Used by:
|
|
173
|
+
- User code: Direct function calls
|
|
174
|
+
- Examples: Quick scripts and tutorials
|
|
175
|
+
- Tests: test_llm_do.py and test_llm_do_comprehensive.py
|
|
176
|
+
|
|
177
|
+
Related modules:
|
|
178
|
+
- agent.py: Full agent system for complex workflows
|
|
179
|
+
- llm.py: Provider abstraction layer
|
|
180
|
+
|
|
181
|
+
Code Size
|
|
182
|
+
---------
|
|
183
|
+
102 lines (down from 387 after refactoring)
|
|
184
|
+
- Removed duplicate OpenOnion authentication logic
|
|
185
|
+
- Eliminated LiteLLM-specific code
|
|
186
|
+
- Now a pure wrapper around llm.py providers
|
|
187
|
+
|
|
188
|
+
Testing
|
|
189
|
+
-------
|
|
190
|
+
Comprehensive test coverage in:
|
|
191
|
+
- tests/test_llm_do.py: 12 tests (unit + integration)
|
|
192
|
+
- tests/test_llm_do_comprehensive.py: 23 tests (all doc examples)
|
|
193
|
+
- tests/test_real_llm_do.py: Real API integration tests
|
|
194
|
+
|
|
195
|
+
All documentation examples in docs/llm_do.md are tested and validated.
|
|
196
|
+
|
|
197
|
+
Example from Documentation
|
|
198
|
+
--------------------------
|
|
199
|
+
From docs/llm_do.md Quick Start:
|
|
200
|
+
|
|
201
|
+
from connectonion import llm_do
|
|
202
|
+
from pydantic import BaseModel
|
|
203
|
+
|
|
204
|
+
# Simple call
|
|
205
|
+
answer = llm_do("What's 2+2?")
|
|
206
|
+
|
|
207
|
+
# Structured output
|
|
208
|
+
class Analysis(BaseModel):
|
|
209
|
+
sentiment: str
|
|
210
|
+
confidence: float
|
|
211
|
+
keywords: list[str]
|
|
212
|
+
|
|
213
|
+
result = llm_do(
|
|
214
|
+
"I absolutely love this product! Best purchase ever!",
|
|
215
|
+
output=Analysis
|
|
216
|
+
)
|
|
217
|
+
print(result.sentiment) # "positive"
|
|
218
|
+
print(result.confidence) # 0.98
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
from typing import Union, Type, Optional, TypeVar
|
|
222
|
+
from pathlib import Path
|
|
223
|
+
from pydantic import BaseModel
|
|
224
|
+
from .prompts import load_system_prompt
|
|
225
|
+
from .llm import create_llm
|
|
226
|
+
|
|
227
|
+
T = TypeVar('T', bound=BaseModel)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def llm_do(
|
|
231
|
+
input: str,
|
|
232
|
+
output: Optional[Type[T]] = None,
|
|
233
|
+
system_prompt: Optional[Union[str, Path]] = None,
|
|
234
|
+
model: str = "co/gemini-2.5-flash",
|
|
235
|
+
api_key: Optional[str] = None,
|
|
236
|
+
**kwargs
|
|
237
|
+
) -> Union[str, T]:
|
|
238
|
+
"""
|
|
239
|
+
Make a one-shot LLM call with optional structured output.
|
|
240
|
+
|
|
241
|
+
Supports multiple LLM providers:
|
|
242
|
+
- OpenAI: "gpt-4o", "o4-mini", "gpt-3.5-turbo"
|
|
243
|
+
- Anthropic: "claude-3-5-sonnet", "claude-3-5-haiku-20241022"
|
|
244
|
+
- Google: "gemini-2.5-pro", "gemini-2.5-flash"
|
|
245
|
+
- ConnectOnion Managed: "co/gpt-4o", "co/o4-mini" (no API keys needed!)
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
input: The input text/question to send to the LLM
|
|
249
|
+
output: Optional Pydantic model class for structured output
|
|
250
|
+
system_prompt: Optional system prompt (string or file path)
|
|
251
|
+
model: Model name (default: "co/gemini-2.5-flash")
|
|
252
|
+
api_key: Optional API key (uses environment variable if not provided)
|
|
253
|
+
**kwargs: Additional parameters (temperature, max_tokens, etc.)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Either a string response or an instance of the output model
|
|
257
|
+
|
|
258
|
+
Examples:
|
|
259
|
+
>>> # Simple string response with default model
|
|
260
|
+
>>> answer = llm_do("What's 2+2?")
|
|
261
|
+
>>> print(answer) # "4"
|
|
262
|
+
|
|
263
|
+
>>> # With ConnectOnion managed keys (no API key needed!)
|
|
264
|
+
>>> answer = llm_do("What's 2+2?", model="co/o4-mini")
|
|
265
|
+
|
|
266
|
+
>>> # With Claude
|
|
267
|
+
>>> answer = llm_do("Explain quantum physics", model="claude-3-5-haiku-20241022")
|
|
268
|
+
|
|
269
|
+
>>> # With Gemini
|
|
270
|
+
>>> answer = llm_do("Write a poem", model="gemini-2.5-flash")
|
|
271
|
+
|
|
272
|
+
>>> # With structured output
|
|
273
|
+
>>> class Analysis(BaseModel):
|
|
274
|
+
... sentiment: str
|
|
275
|
+
... score: float
|
|
276
|
+
>>>
|
|
277
|
+
>>> result = llm_do("I love this!", output=Analysis)
|
|
278
|
+
>>> print(result.sentiment) # "positive"
|
|
279
|
+
"""
|
|
280
|
+
# Validate input
|
|
281
|
+
if not input or not input.strip():
|
|
282
|
+
raise ValueError("Input cannot be empty")
|
|
283
|
+
|
|
284
|
+
# Load system prompt
|
|
285
|
+
if system_prompt:
|
|
286
|
+
prompt_text = load_system_prompt(system_prompt)
|
|
287
|
+
else:
|
|
288
|
+
prompt_text = "You are a helpful assistant."
|
|
289
|
+
|
|
290
|
+
# Build messages
|
|
291
|
+
messages = [
|
|
292
|
+
{"role": "system", "content": prompt_text},
|
|
293
|
+
{"role": "user", "content": input}
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
# Create LLM using factory (only pass api_key and initialization params)
|
|
297
|
+
llm = create_llm(model=model, api_key=api_key)
|
|
298
|
+
|
|
299
|
+
# Get response
|
|
300
|
+
if output:
|
|
301
|
+
# Structured output - use structured_complete()
|
|
302
|
+
return llm.structured_complete(messages, output, **kwargs)
|
|
303
|
+
else:
|
|
304
|
+
# Plain text - use complete()
|
|
305
|
+
# Pass through kwargs (max_tokens, temperature, etc.)
|
|
306
|
+
response = llm.complete(messages, tools=None, **kwargs)
|
|
307
|
+
return response.content
|
connectonion/logger.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Unified logging interface for agents - terminal output + plain text + YAML sessions
|
|
3
|
+
LLM-Note:
|
|
4
|
+
Dependencies: imports from [datetime, pathlib, typing, yaml, console.py] | imported by [agent.py, tool_executor.py] | tested by [tests/unit/test_logger.py]
|
|
5
|
+
Data flow: receives from Agent/tool_executor → delegates to Console for terminal/file → writes YAML sessions to .co/sessions/
|
|
6
|
+
State/Effects: writes to .co/sessions/{agent_name}.yaml (one file per agent, appends turns) | delegates file logging to Console | session data persisted after each turn
|
|
7
|
+
Integration: exposes Logger(agent_name, quiet, log), .print(), .log_tool_call(name, args), .log_tool_result(result, timing), .log_llm_response(), .start_session(), .log_turn()
|
|
8
|
+
Session format: metadata at top → turns summary (with tools_called as function-call style) → system_prompt + messages at end (see docs/session-yaml-format.md)
|
|
9
|
+
Performance: YAML written after each turn (incremental) | loads existing session file on start | Console delegation is direct passthrough
|
|
10
|
+
Errors: let I/O errors bubble up (no try-except)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, Union, Dict, Any
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from .console import Console
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Logger:
|
|
22
|
+
"""Unified logging: terminal output + plain text + YAML sessions.
|
|
23
|
+
|
|
24
|
+
Facade pattern: wraps Console for terminal/file logging, adds YAML sessions.
|
|
25
|
+
|
|
26
|
+
Session files use one file per agent (.co/sessions/{agent_name}.yaml) to
|
|
27
|
+
reduce file clutter. New turns are appended to the same file.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
agent_name: Name of the agent (used in filenames)
|
|
31
|
+
quiet: Suppress console output (default False)
|
|
32
|
+
log: Enable file logging (default True, or path string for custom location)
|
|
33
|
+
|
|
34
|
+
Files created:
|
|
35
|
+
- .co/logs/{agent_name}.log: Plain text log with session markers
|
|
36
|
+
- .co/sessions/{agent_name}.yaml: Structured YAML with all turns
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
# Development (default) - see output + save everything
|
|
40
|
+
logger = Logger("my-agent")
|
|
41
|
+
|
|
42
|
+
# Eval mode - quiet but record sessions
|
|
43
|
+
logger = Logger("my-agent", quiet=True)
|
|
44
|
+
|
|
45
|
+
# Benchmark - completely off
|
|
46
|
+
logger = Logger("my-agent", log=False)
|
|
47
|
+
|
|
48
|
+
# Custom log path
|
|
49
|
+
logger = Logger("my-agent", log="custom/path.log")
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
agent_name: str,
|
|
55
|
+
quiet: bool = False,
|
|
56
|
+
log: Union[bool, str, Path, None] = None
|
|
57
|
+
):
|
|
58
|
+
self.agent_name = agent_name
|
|
59
|
+
|
|
60
|
+
# Determine what to enable
|
|
61
|
+
self.enable_console = not quiet
|
|
62
|
+
self.enable_sessions = True # Sessions on unless log=False
|
|
63
|
+
self.enable_file = True
|
|
64
|
+
self.log_file_path = Path(f".co/logs/{agent_name}.log")
|
|
65
|
+
|
|
66
|
+
# Parse log parameter
|
|
67
|
+
if log is False:
|
|
68
|
+
# log=False: disable everything
|
|
69
|
+
self.enable_file = False
|
|
70
|
+
self.enable_sessions = False
|
|
71
|
+
elif isinstance(log, (str, Path)) and log:
|
|
72
|
+
# Custom path
|
|
73
|
+
self.log_file_path = Path(log)
|
|
74
|
+
# else: log=True or log=None → defaults
|
|
75
|
+
|
|
76
|
+
# If quiet=True, also disable file (only keep sessions)
|
|
77
|
+
if quiet:
|
|
78
|
+
self.enable_file = False
|
|
79
|
+
|
|
80
|
+
# Console for terminal output (only if not quiet)
|
|
81
|
+
self.console = None
|
|
82
|
+
if self.enable_console:
|
|
83
|
+
file_path = self.log_file_path if self.enable_file else None
|
|
84
|
+
self.console = Console(log_file=file_path)
|
|
85
|
+
|
|
86
|
+
# Session state (YAML)
|
|
87
|
+
self.session_file: Optional[Path] = None
|
|
88
|
+
self.session_data: Optional[Dict[str, Any]] = None
|
|
89
|
+
|
|
90
|
+
# Delegate to Console
|
|
91
|
+
def print(self, message: str, style: str = None):
|
|
92
|
+
"""Print message to console (if enabled)."""
|
|
93
|
+
if self.console:
|
|
94
|
+
self.console.print(message, style)
|
|
95
|
+
|
|
96
|
+
def print_xray_table(self, *args, **kwargs):
|
|
97
|
+
"""Print xray table for decorated tools."""
|
|
98
|
+
if self.console:
|
|
99
|
+
self.console.print_xray_table(*args, **kwargs)
|
|
100
|
+
|
|
101
|
+
def log_llm_response(self, *args, **kwargs):
|
|
102
|
+
"""Log LLM response with token usage."""
|
|
103
|
+
if self.console:
|
|
104
|
+
self.console.log_llm_response(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
def log_tool_call(self, tool_name: str, tool_args: dict):
|
|
107
|
+
"""Log tool call."""
|
|
108
|
+
if self.console:
|
|
109
|
+
self.console.log_tool_call(tool_name, tool_args)
|
|
110
|
+
|
|
111
|
+
def log_tool_result(self, result: str, timing_ms: float):
|
|
112
|
+
"""Log tool result."""
|
|
113
|
+
if self.console:
|
|
114
|
+
self.console.log_tool_result(result, timing_ms)
|
|
115
|
+
|
|
116
|
+
def _format_tool_call(self, trace_entry: dict) -> str:
|
|
117
|
+
"""Format tool call as natural function-call style: greet(name='Alice')"""
|
|
118
|
+
tool_name = trace_entry.get('tool_name', '')
|
|
119
|
+
args = trace_entry.get('arguments', {})
|
|
120
|
+
parts = []
|
|
121
|
+
for k, v in args.items():
|
|
122
|
+
if isinstance(v, str):
|
|
123
|
+
v_str = v if len(v) <= 50 else v[:50] + "..."
|
|
124
|
+
parts.append(f"{k}='{v_str}'")
|
|
125
|
+
else:
|
|
126
|
+
v_str = str(v)
|
|
127
|
+
if len(v_str) > 50:
|
|
128
|
+
v_str = v_str[:50] + "..."
|
|
129
|
+
parts.append(f"{k}={v_str}")
|
|
130
|
+
return f"{tool_name}({', '.join(parts)})"
|
|
131
|
+
|
|
132
|
+
# Session logging (YAML)
|
|
133
|
+
def start_session(self, system_prompt: str = "", session_id: Optional[str] = None):
|
|
134
|
+
"""Initialize session YAML file.
|
|
135
|
+
|
|
136
|
+
Uses one file per session_id (for HTTP API) or per agent (for interactive).
|
|
137
|
+
Loads existing session data if file exists, appends new turns.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
system_prompt: The system prompt for this session
|
|
141
|
+
session_id: Optional session identifier. If provided, logs to
|
|
142
|
+
.co/sessions/{session_id}.yaml for thread-safe HTTP API.
|
|
143
|
+
If None, uses agent name for interactive mode.
|
|
144
|
+
"""
|
|
145
|
+
if not self.enable_sessions:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
sessions_dir = Path(".co/sessions")
|
|
149
|
+
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
|
|
151
|
+
# Use session_id if provided (HTTP API), otherwise use agent_name (interactive)
|
|
152
|
+
filename = session_id if session_id else self.agent_name
|
|
153
|
+
# Sanitize: keep only safe characters (alphanumeric, dash, underscore)
|
|
154
|
+
import re
|
|
155
|
+
filename = re.sub(r'[^a-zA-Z0-9_-]', '_', filename)[:255] or 'default'
|
|
156
|
+
self.session_file = sessions_dir / f"{filename}.yaml"
|
|
157
|
+
|
|
158
|
+
# Load existing session or create new
|
|
159
|
+
if self.session_file.exists():
|
|
160
|
+
with open(self.session_file, 'r') as f:
|
|
161
|
+
self.session_data = yaml.safe_load(f) or {}
|
|
162
|
+
# Ensure ALL required fields exist (handles empty/corrupted files)
|
|
163
|
+
if 'name' not in self.session_data:
|
|
164
|
+
self.session_data['name'] = self.agent_name
|
|
165
|
+
if 'session_id' not in self.session_data and session_id:
|
|
166
|
+
self.session_data['session_id'] = session_id
|
|
167
|
+
if 'created' not in self.session_data:
|
|
168
|
+
self.session_data['created'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
169
|
+
if 'total_cost' not in self.session_data:
|
|
170
|
+
self.session_data['total_cost'] = 0.0
|
|
171
|
+
if 'total_tokens' not in self.session_data:
|
|
172
|
+
self.session_data['total_tokens'] = 0
|
|
173
|
+
if 'turns' not in self.session_data:
|
|
174
|
+
self.session_data['turns'] = []
|
|
175
|
+
if 'messages' not in self.session_data:
|
|
176
|
+
self.session_data['messages'] = {}
|
|
177
|
+
# Update system_prompt if provided
|
|
178
|
+
if system_prompt:
|
|
179
|
+
self.session_data['system_prompt'] = system_prompt
|
|
180
|
+
else:
|
|
181
|
+
self.session_data = {
|
|
182
|
+
"name": self.agent_name,
|
|
183
|
+
"session_id": session_id,
|
|
184
|
+
"created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
185
|
+
"total_cost": 0.0,
|
|
186
|
+
"total_tokens": 0,
|
|
187
|
+
"system_prompt": system_prompt,
|
|
188
|
+
"turns": [],
|
|
189
|
+
"messages": {} # Dict keyed by turn number
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def log_turn(self, user_input: str, result: str, duration_ms: float, session: dict, model: str):
|
|
193
|
+
"""Log turn summary + messages to YAML file.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
user_input: The user's input prompt
|
|
197
|
+
result: The agent's final response
|
|
198
|
+
duration_ms: Total duration in milliseconds
|
|
199
|
+
session: Agent's current_session dict (contains messages, trace)
|
|
200
|
+
model: Model name string
|
|
201
|
+
"""
|
|
202
|
+
if not self.enable_sessions or not self.session_data:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
# Aggregate from trace
|
|
206
|
+
trace = session.get('trace', [])
|
|
207
|
+
llm_calls = [t for t in trace if t.get('type') == 'llm_call']
|
|
208
|
+
tool_calls = [t for t in trace if t.get('type') == 'tool_execution']
|
|
209
|
+
|
|
210
|
+
total_tokens = sum(
|
|
211
|
+
(t.get('usage').input_tokens + t.get('usage').output_tokens)
|
|
212
|
+
for t in llm_calls if t.get('usage')
|
|
213
|
+
)
|
|
214
|
+
total_cost = sum(
|
|
215
|
+
t.get('usage').cost
|
|
216
|
+
for t in llm_calls if t.get('usage')
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
turn_data = {
|
|
220
|
+
'input': user_input,
|
|
221
|
+
'expected': session.get('expected', ''),
|
|
222
|
+
'model': model,
|
|
223
|
+
'duration_ms': int(duration_ms),
|
|
224
|
+
'tokens': total_tokens,
|
|
225
|
+
'cost': round(total_cost, 4),
|
|
226
|
+
'tools_called': [self._format_tool_call(t) for t in tool_calls],
|
|
227
|
+
'result': result,
|
|
228
|
+
'evaluation': session.get('evaluation', '')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# Update session aggregates
|
|
232
|
+
self.session_data['updated'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
233
|
+
self.session_data['total_cost'] = round(
|
|
234
|
+
self.session_data.get('total_cost', 0) + turn_data['cost'], 4
|
|
235
|
+
)
|
|
236
|
+
self.session_data['total_tokens'] = (
|
|
237
|
+
self.session_data.get('total_tokens', 0) + turn_data['tokens']
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Add turn number and timestamp
|
|
241
|
+
turn_num = len(self.session_data['turns']) + 1
|
|
242
|
+
turn_data['turn'] = turn_num
|
|
243
|
+
turn_data['timestamp'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
244
|
+
self.session_data['turns'].append(turn_data)
|
|
245
|
+
|
|
246
|
+
# Extract this turn's messages (everything after what we've already saved)
|
|
247
|
+
all_messages = session.get('messages', [])
|
|
248
|
+
saved_count = sum(len(msgs) for msgs in self.session_data['messages'].values())
|
|
249
|
+
turn_messages = all_messages[saved_count + 1:] # +1 to skip system message
|
|
250
|
+
self.session_data['messages'][turn_num] = turn_messages
|
|
251
|
+
|
|
252
|
+
# Write YAML
|
|
253
|
+
self._write_session()
|
|
254
|
+
|
|
255
|
+
def _write_session(self):
|
|
256
|
+
"""Write session data with turns summary first, detail at end."""
|
|
257
|
+
# Build ordered dict: compact metadata → turns → detail (system_prompt + messages)
|
|
258
|
+
ordered = {
|
|
259
|
+
'name': self.session_data['name'],
|
|
260
|
+
'session_id': self.session_data.get('session_id'),
|
|
261
|
+
'created': self.session_data['created'],
|
|
262
|
+
'updated': self.session_data.get('updated', ''),
|
|
263
|
+
'total_cost': self.session_data.get('total_cost', 0),
|
|
264
|
+
'total_tokens': self.session_data.get('total_tokens', 0),
|
|
265
|
+
'turns': self.session_data['turns'],
|
|
266
|
+
# Detail section (scroll down)
|
|
267
|
+
'system_prompt': self.session_data.get('system_prompt', ''),
|
|
268
|
+
'messages': self.session_data['messages']
|
|
269
|
+
}
|
|
270
|
+
with open(self.session_file, 'w') as f:
|
|
271
|
+
yaml.dump(ordered, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
272
|
+
|
|
273
|
+
def load_messages(self) -> list:
|
|
274
|
+
"""Load and reconstruct full message list from session file.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Full message list: [system_message] + all turn messages in order
|
|
278
|
+
"""
|
|
279
|
+
if not self.session_file or not self.session_file.exists():
|
|
280
|
+
return []
|
|
281
|
+
with open(self.session_file, 'r') as f:
|
|
282
|
+
data = yaml.safe_load(f) or {}
|
|
283
|
+
|
|
284
|
+
# Reconstruct: system prompt + all turn messages in order
|
|
285
|
+
messages = []
|
|
286
|
+
if data.get('system_prompt'):
|
|
287
|
+
messages.append({"role": "system", "content": data['system_prompt']})
|
|
288
|
+
|
|
289
|
+
turn_messages = data.get('messages', {})
|
|
290
|
+
for turn_num in sorted(turn_messages.keys()):
|
|
291
|
+
messages.extend(turn_messages[turn_num])
|
|
292
|
+
|
|
293
|
+
return messages
|
|
294
|
+
|
|
295
|
+
def load_session(self) -> dict:
|
|
296
|
+
"""Load session data from file."""
|
|
297
|
+
if not self.session_file or not self.session_file.exists():
|
|
298
|
+
return {'system_prompt': '', 'turns': [], 'messages': {}}
|
|
299
|
+
with open(self.session_file, 'r') as f:
|
|
300
|
+
return yaml.safe_load(f) or {'system_prompt': '', 'turns': [], 'messages': {}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Prompts module for ConnectOnion built-in plugins."""
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
You are a CRM analyst helping categorize email contacts for a personal CRM.
|
|
2
|
+
|
|
3
|
+
CRITICAL: Distinguish between REAL CONTACTS (people worth tracking) vs SERVICE EMAILS (automated/marketing).
|
|
4
|
+
|
|
5
|
+
## First, Determine Contact Type
|
|
6
|
+
|
|
7
|
+
**REAL CONTACT** (worth tracking):
|
|
8
|
+
- Actual person you've had conversations with
|
|
9
|
+
- Business contacts, clients, partners, colleagues
|
|
10
|
+
- Friends, family, professional connections
|
|
11
|
+
- People who sent you personalized messages
|
|
12
|
+
|
|
13
|
+
**SERVICE/AUTOMATED** (low priority):
|
|
14
|
+
- Product update emails from tools you use (OneUp, Calendly, etc.)
|
|
15
|
+
- Notification emails (LinkedIn, X/Twitter, GitHub, etc.)
|
|
16
|
+
- Marketing/promotional emails
|
|
17
|
+
- Receipts, invoices, shipping notifications
|
|
18
|
+
- Newsletter subscriptions
|
|
19
|
+
- No-reply addresses
|
|
20
|
+
|
|
21
|
+
## Analysis Format
|
|
22
|
+
|
|
23
|
+
### 1. Contact Type
|
|
24
|
+
- **REAL_PERSON**: Worth tracking in CRM
|
|
25
|
+
- **SERVICE_TOOL**: SaaS/tool you use (low priority)
|
|
26
|
+
- **NOTIFICATION**: Social media, platform notifications (skip)
|
|
27
|
+
- **MARKETING**: Promotional/sales emails (skip)
|
|
28
|
+
|
|
29
|
+
### 2. Priority Score (1-10)
|
|
30
|
+
- 10: Key business contact, active relationship
|
|
31
|
+
- 7-9: Regular professional contact
|
|
32
|
+
- 4-6: Occasional contact, might be useful
|
|
33
|
+
- 1-3: Service/automated, not worth tracking
|
|
34
|
+
|
|
35
|
+
### 3. If REAL_PERSON, provide:
|
|
36
|
+
- Relationship context (colleague, client, friend, vendor)
|
|
37
|
+
- Key topics discussed
|
|
38
|
+
- Communication pattern
|
|
39
|
+
- Important notes
|
|
40
|
+
- Suggested tags (#client, #technical, #partner, etc.)
|
|
41
|
+
|
|
42
|
+
### 4. If SERVICE/AUTOMATED, provide:
|
|
43
|
+
- What service/tool this is from
|
|
44
|
+
- Why it's not a real contact
|
|
45
|
+
- Recommendation: SKIP (don't store) or LOW_PRIORITY
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
**Example 1: davis@oneupapp.io**
|
|
50
|
+
- Type: SERVICE_TOOL
|
|
51
|
+
- Priority: 2
|
|
52
|
+
- Analysis: OneUp is a social media scheduling tool. These emails are product updates and marketing, not personal communication.
|
|
53
|
+
- Recommendation: SKIP - not a real relationship
|
|
54
|
+
|
|
55
|
+
**Example 2: john.smith@acme.com**
|
|
56
|
+
- Type: REAL_PERSON
|
|
57
|
+
- Priority: 8
|
|
58
|
+
- Relationship: Potential client
|
|
59
|
+
- Topics: Discussed product demo, pricing inquiry
|
|
60
|
+
- Tags: #prospect, #sales, #enterprise
|
|
61
|
+
|
|
62
|
+
Be concise and factual. The goal is to help build a meaningful CRM, not clutter it with service notifications.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
You generate expected outcomes for AI agent tasks.
|
|
2
|
+
|
|
3
|
+
Given a user request and available tools, describe what should happen to complete the task.
|
|
4
|
+
|
|
5
|
+
Be concise (1-2 sentences). Focus on:
|
|
6
|
+
- What tools should be used
|
|
7
|
+
- What the result should contain
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
- "Read the file, then provide a summary of its contents."
|
|
11
|
+
- "Search for Python tutorials, then list the top 3 results."
|
|
12
|
+
- "Calculate the expression and return the numeric answer."
|