Skip to content

Commit e90142c

Browse files
committed
Add AI cell support to PyodideRuntimeAgent
- Add OpenAI client with tool calling support for creating cells - Enable AI cell execution alongside Python code execution - Add notebook context gathering for AI awareness - Support create_cell tool for AI to modify notebooks - Add strip-ansi dependency for cleaning tracebacks - Update capabilities to include AI execution - Tests!
1 parent c3aae04 commit e90142c

File tree

12 files changed

+2104
-11
lines changed

12 files changed

+2104
-11
lines changed

HANDOFF.md

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# HANDOFF: AI Cells Integration for Runt PyodideRuntimeAgent
2+
3+
**Date**: January 2025\
4+
**Branch**: `ai-cells-integration`\
5+
**Status**: Working prototype ready for merge\
6+
**Context**: Basic AI integration functional, provider abstraction needed next
7+
8+
## What This Delivers
9+
10+
Added AI cell support to runt's PyodideRuntimeAgent. This is a working prototype
11+
that demonstrates AI cells executing alongside Python cells with basic OpenAI
12+
integration.
13+
14+
**Core functionality working**:
15+
16+
- AI cells execute when `cellType: "ai"` is set
17+
- OpenAI API integration with real API calls
18+
- Basic tool calling - AI can create new cells
19+
- Context awareness - AI sees previous cells and outputs
20+
- Graceful fallback to mock responses without API key
21+
22+
**Current limitations**:
23+
24+
- Hardcoded to OpenAI only
25+
- Single tool function (`create_cell`)
26+
- No streaming responses
27+
- No provider abstraction
28+
- No deterministic testing model
29+
30+
## Files Changed
31+
32+
### New Files
33+
34+
```
35+
packages/pyodide-runtime-agent/src/openai-client.ts # OpenAI API wrapper
36+
packages/pyodide-runtime-agent/test/ai-cell-integration.test.ts # AI tests
37+
```
38+
39+
### Modified Files
40+
41+
```
42+
packages/pyodide-runtime-agent/src/pyodide-agent.ts # AI execution pipeline
43+
packages/pyodide-runtime-agent/deno.json # Dependencies
44+
packages/pyodide-runtime-agent/src/lib.ts # Exports
45+
packages/pyodide-runtime-agent/src/mod.ts # Exports
46+
```
47+
48+
### Dependencies Added
49+
50+
```json
51+
{
52+
"@openai/openai": "jsr:@openai/openai@^4.98.0",
53+
"npm:strip-ansi": "npm:strip-ansi@^7.1.0"
54+
}
55+
```
56+
57+
## How It Works
58+
59+
1. **Cell Detection**: `executePython()` detects `cellType === "ai"`
60+
2. **Context Gathering**: Collects previous cells and their text outputs
61+
3. **API Call**: Sends to OpenAI with `create_cell` tool enabled
62+
4. **Tool Execution**: AI can create new cells with proper positioning
63+
5. **Output**: Returns markdown response with tool call tracking
64+
65+
## Usage
66+
67+
```bash
68+
# Set API key for real responses
69+
export OPENAI_API_KEY="your-key"
70+
71+
# Start runtime
72+
NOTEBOOK_ID=test AUTH_TOKEN=token deno run --allow-all src/mod.ts
73+
```
74+
75+
AI cells work like Python cells but with `cellType: "ai"`:
76+
77+
```typescript
78+
store.commit(events.cellCreated({
79+
id: "ai-cell-1",
80+
cellType: "ai",
81+
position: 1,
82+
createdBy: "user",
83+
}));
84+
85+
store.commit(events.cellSourceChanged({
86+
id: "ai-cell-1",
87+
source: "Create a Python cell that plots a sine wave",
88+
modifiedBy: "user",
89+
}));
90+
```
91+
92+
## Testing
93+
94+
Tests work with or without API key:
95+
96+
- With `OPENAI_API_KEY`: Tests real OpenAI responses
97+
- Without key: Uses mock responses
98+
99+
```bash
100+
deno task ci # Full test suite
101+
```
102+
103+
## Next Steps Needed
104+
105+
This prototype proves AI cells work but needs:
106+
107+
1. **Provider abstraction** for different AI services
108+
2. **Streaming responses** (requires schema changes)
109+
3. **More tools** (`modify_cell`, `execute_cell`)
110+
4. **Deterministic test model**
111+
5. **Model selection UI**
112+
113+
## Technical Notes
114+
115+
- All typing is clean with schema interfaces
116+
- Error handling for API failures
117+
- Context filtering (text outputs only)
118+
- Tool calls tracked in output metadata
119+
- Session management works across restarts
120+
121+
## Ready to Merge
122+
123+
This is a functional prototype that demonstrates AI integration without breaking
124+
existing functionality. The hardcoded OpenAI approach is fine for proving the
125+
concept - provider abstraction can come next.
126+
127+
**What works**: Basic AI assistance with cell creation **What's limited**:
128+
Single provider, single tool, no streaming **What's next**: Provider system and
129+
expanded capabilities
130+
131+
````
132+
Now for the provider plan:
133+
134+
## Provider Architecture Plan
135+
136+
### 1. Provider Interface
137+
138+
Create a generic provider interface that all AI services implement:
139+
140+
```typescript
141+
interface AIProvider {
142+
name: string;
143+
models: string[];
144+
145+
// Core execution
146+
executePrompt(params: {
147+
messages: ChatMessage[];
148+
model?: string;
149+
tools?: Tool[];
150+
systemPrompt?: string;
151+
}): Promise<AIResponse>;
152+
153+
// Streaming (future)
154+
executePromptStream(params: ExecuteParams): AsyncIterableIterator<AIStreamChunk>;
155+
156+
// Capabilities
157+
supportsTools(): boolean;
158+
supportsStreaming(): boolean;
159+
getAvailableModels(): Promise<string[]>;
160+
}
161+
````
162+
163+
### 2. Provider Implementations
164+
165+
**OpenAIProvider** (current functionality)
166+
167+
- Uses existing OpenAI client
168+
- Supports tools and streaming
169+
- Model selection from OpenAI's API
170+
171+
**OllamaProvider** (local models)
172+
173+
- HTTP calls to local Ollama instance
174+
- Model list from `/api/tags`
175+
- Tools support varies by model
176+
177+
**TestProvider** (deterministic testing)
178+
179+
- Hardcoded responses
180+
- Predictable outputs for CI
181+
- Tool calling simulation
182+
183+
### 3. Provider Registry
184+
185+
```typescript
186+
class ProviderRegistry {
187+
private providers = new Map<string, AIProvider>();
188+
189+
register(provider: AIProvider): void;
190+
get(name: string): AIProvider | undefined;
191+
list(): AIProvider[];
192+
getDefault(): AIProvider;
193+
}
194+
```
195+
196+
### 4. Schema Changes for Streaming
197+
198+
For streaming AI responses, we need delta updates:
199+
200+
```typescript
201+
// New event for updating existing outputs
202+
interface OutputDeltaEvent {
203+
cellId: string;
204+
outputId: string;
205+
delta: {
206+
text?: string; // Append to existing text
207+
data?: Record<string, unknown>; // Merge with existing data
208+
metadata?: Record<string, unknown>; // Merge metadata
209+
};
210+
}
211+
```
212+
213+
This lets us:
214+
215+
- Stream AI responses in real-time
216+
- Update outputs incrementally
217+
- Show progress indicators
218+
- Handle interruptions cleanly
219+
220+
### 5. Configuration
221+
222+
Provider selection through environment or config:
223+
224+
```typescript
225+
interface AIConfig {
226+
defaultProvider: string;
227+
providers: {
228+
openai?: { apiKey: string; baseURL?: string };
229+
ollama?: { baseURL: string };
230+
test?: { responses: Record<string, string> };
231+
};
232+
}
233+
```
234+
235+
### 6. Implementation Order
236+
237+
1. **Provider interface** - Define the contract
238+
2. **Refactor existing OpenAI code** - Make it a provider
239+
3. **Add TestProvider** - For reliable testing
240+
4. **Registry and configuration** - Provider selection
241+
5. **OllamaProvider** - Local model support
242+
6. **Streaming schema changes** - Delta updates
243+
7. **Streaming implementation** - Real-time responses
244+
245+
This gives you a clean path from the current prototype to a flexible,
246+
multi-provider system with streaming support.

deno.lock

Lines changed: 19 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/lib/src/runtime-agent.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export class RuntimeAgent {
3535
private subscriptions: (() => void)[] = [];
3636
private activeExecutions = new Map<string, AbortController>();
3737
private cancellationHandlers: CancellationHandler[] = [];
38+
private signalHandlers = new Map<string, () => void>();
3839

3940
constructor(
4041
private config: RuntimeConfig,
@@ -162,6 +163,9 @@ export class RuntimeAgent {
162163
}
163164
}
164165

166+
// Clean up signal handlers
167+
this.cleanupSignalHandlers();
168+
165169
// Close LiveStore connection
166170
if (this.store) {
167171
await this.store.shutdown?.();
@@ -645,8 +649,12 @@ export class RuntimeAgent {
645649
private setupShutdownHandlers(): void {
646650
const shutdown = () => this.shutdown();
647651

648-
Deno.addSignalListener("SIGINT", shutdown);
649-
Deno.addSignalListener("SIGTERM", shutdown);
652+
// Store signal handlers for cleanup
653+
this.signalHandlers.set("SIGINT", shutdown);
654+
this.signalHandlers.set("SIGTERM", shutdown);
655+
656+
Deno.addSignalListener("SIGINT" as Deno.Signal, shutdown);
657+
Deno.addSignalListener("SIGTERM" as Deno.Signal, shutdown);
650658

651659
globalThis.addEventListener("unhandledrejection", (event) => {
652660
const errorLogger = createLogger(`${this.config.kernelType}-agent`);
@@ -675,6 +683,25 @@ export class RuntimeAgent {
675683
});
676684
}
677685

686+
/**
687+
* Clean up signal handlers
688+
*/
689+
private cleanupSignalHandlers(): void {
690+
for (const [signal, handler] of this.signalHandlers) {
691+
try {
692+
Deno.removeSignalListener(signal as Deno.Signal, handler);
693+
} catch (error) {
694+
// Ignore errors during cleanup
695+
const cleanupLogger = createLogger(`${this.config.kernelType}-agent`);
696+
cleanupLogger.debug("Error removing signal listener", {
697+
signal,
698+
error: error instanceof Error ? error.message : String(error),
699+
});
700+
}
701+
}
702+
this.signalHandlers.clear();
703+
}
704+
678705
/**
679706
* Keep the agent alive until shutdown
680707
*/

packages/pyodide-runtime-agent/deno.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"@runt/schema": "jsr:@runt/schema@^0.1.0",
2020
"npm:pyodide": "npm:pyodide@^0.27.7",
2121
"@std/async": "jsr:@std/async@^1.0.0",
22-
"npm:@livestore/livestore": "npm:@livestore/livestore@^0.3.1"
22+
"npm:@livestore/livestore": "npm:@livestore/livestore@^0.3.1",
23+
"@openai/openai": "jsr:@openai/openai@^4.98.0",
24+
"npm:strip-ansi": "npm:strip-ansi@^7.1.0"
2325
},
2426
"tasks": {
2527
"dev": "deno run --allow-all --env-file=.env src/pyodide-agent.ts",

packages/pyodide-runtime-agent/pyodide-agent.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ Deno.test("PyodideRuntimeAgent configuration", async (t) => {
134134
assertEquals(agent.config.kernelType, "python3-pyodide");
135135
assertEquals(agent.config.capabilities.canExecuteCode, true);
136136
assertEquals(agent.config.capabilities.canExecuteSql, false);
137-
assertEquals(agent.config.capabilities.canExecuteAi, false);
137+
assertEquals(agent.config.capabilities.canExecuteAi, true);
138138
});
139139

140140
await t.step("should have correct kernel type and capabilities", () => {
@@ -151,7 +151,7 @@ Deno.test("PyodideRuntimeAgent configuration", async (t) => {
151151
assertEquals(agent.config.kernelType, "python3-pyodide");
152152
assertEquals(agent.config.capabilities.canExecuteCode, true);
153153
assertEquals(agent.config.capabilities.canExecuteSql, false);
154-
assertEquals(agent.config.capabilities.canExecuteAi, false);
154+
assertEquals(agent.config.capabilities.canExecuteAi, true);
155155
});
156156
});
157157

0 commit comments

Comments
 (0)