@soederpop/luca 0.0.31 → 0.0.34
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.
- package/README.md +241 -36
- package/bun.lock +24 -5
- package/commands/build-python-bridge.ts +43 -0
- package/docs/apis/clients/rest.md +7 -7
- package/docs/apis/clients/websocket.md +23 -10
- package/docs/apis/features/agi/assistant.md +155 -8
- package/docs/apis/features/agi/assistants-manager.md +90 -22
- package/docs/apis/features/agi/auto-assistant.md +377 -0
- package/docs/apis/features/agi/browser-use.md +802 -0
- package/docs/apis/features/agi/claude-code.md +6 -1
- package/docs/apis/features/agi/conversation-history.md +7 -6
- package/docs/apis/features/agi/conversation.md +111 -38
- package/docs/apis/features/agi/docs-reader.md +35 -57
- package/docs/apis/features/agi/file-tools.md +163 -0
- package/docs/apis/features/agi/openapi.md +2 -2
- package/docs/apis/features/agi/skills-library.md +227 -0
- package/docs/apis/features/node/content-db.md +125 -4
- package/docs/apis/features/node/disk-cache.md +11 -11
- package/docs/apis/features/node/downloader.md +1 -1
- package/docs/apis/features/node/file-manager.md +15 -15
- package/docs/apis/features/node/fs.md +78 -21
- package/docs/apis/features/node/git.md +50 -10
- package/docs/apis/features/node/google-calendar.md +3 -0
- package/docs/apis/features/node/google-docs.md +10 -1
- package/docs/apis/features/node/google-drive.md +3 -0
- package/docs/apis/features/node/google-mail.md +214 -0
- package/docs/apis/features/node/google-sheets.md +3 -0
- package/docs/apis/features/node/ink.md +10 -10
- package/docs/apis/features/node/ipc-socket.md +83 -93
- package/docs/apis/features/node/networking.md +5 -5
- package/docs/apis/features/node/os.md +7 -7
- package/docs/apis/features/node/package-finder.md +14 -14
- package/docs/apis/features/node/proc.md +2 -1
- package/docs/apis/features/node/process-manager.md +70 -3
- package/docs/apis/features/node/python.md +265 -9
- package/docs/apis/features/node/redis.md +380 -0
- package/docs/apis/features/node/ui.md +13 -13
- package/docs/apis/servers/express.md +35 -7
- package/docs/apis/servers/mcp.md +3 -3
- package/docs/apis/servers/websocket.md +51 -8
- package/docs/bootstrap/CLAUDE.md +1 -1
- package/docs/bootstrap/SKILL.md +93 -7
- package/docs/examples/feature-as-tool-provider.md +143 -0
- package/docs/examples/python.md +42 -1
- package/docs/introspection.md +15 -5
- package/docs/tutorials/00-bootstrap.md +3 -3
- package/docs/tutorials/02-container.md +2 -2
- package/docs/tutorials/10-creating-features.md +5 -0
- package/docs/tutorials/13-introspection.md +12 -2
- package/docs/tutorials/19-python-sessions.md +401 -0
- package/package.json +8 -4
- package/src/agi/container.server.ts +8 -0
- package/src/agi/features/assistant.ts +19 -0
- package/src/agi/features/autonomous-assistant.ts +435 -0
- package/src/agi/features/conversation.ts +58 -6
- package/src/agi/features/file-tools.ts +286 -0
- package/src/agi/features/luca-coder.ts +643 -0
- package/src/bootstrap/generated.ts +705 -17
- package/src/cli/build-info.ts +2 -2
- package/src/cli/cli.ts +22 -13
- package/src/commands/bootstrap.ts +49 -6
- package/src/commands/code.ts +369 -0
- package/src/commands/describe.ts +7 -2
- package/src/commands/index.ts +1 -0
- package/src/commands/sandbox-mcp.ts +7 -7
- package/src/commands/save-api-docs.ts +1 -1
- package/src/container-describer.ts +4 -4
- package/src/container.ts +10 -19
- package/src/helper.ts +24 -33
- package/src/introspection/generated.agi.ts +2499 -63
- package/src/introspection/generated.node.ts +1625 -688
- package/src/introspection/generated.web.ts +15 -57
- package/src/node/container.ts +5 -0
- package/src/node/features/figlet-fonts.ts +597 -0
- package/src/node/features/fs.ts +3 -9
- package/src/node/features/helpers.ts +20 -0
- package/src/node/features/python.ts +429 -16
- package/src/node/features/redis.ts +446 -0
- package/src/node/features/ui.ts +4 -11
- package/src/python/bridge.py +220 -0
- package/src/python/generated.ts +227 -0
- package/src/scaffolds/generated.ts +1 -1
- package/test/python-session.test.ts +105 -0
- package/assistants/lucaExpert/CORE.md +0 -37
- package/assistants/lucaExpert/hooks.ts +0 -9
- package/assistants/lucaExpert/tools.ts +0 -177
package/docs/introspection.md
CHANGED
|
@@ -7,7 +7,7 @@ Luca's introspection system lets you discover everything about a container and i
|
|
|
7
7
|
The container knows what it is, what registries it has, and what's available in each one.
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
|
-
const info = container.
|
|
10
|
+
const info = container.introspect()
|
|
11
11
|
console.log(info.className)
|
|
12
12
|
console.log(info.registries.map(r => `${r.name}: ${r.available.length} available`))
|
|
13
13
|
console.log('factories:', info.factoryNames)
|
|
@@ -17,17 +17,17 @@ console.log('enabled features:', info.enabledFeatures)
|
|
|
17
17
|
You can get the full introspection as markdown:
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
console.log(container.
|
|
20
|
+
console.log(container.introspectAsText())
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Or request a single section:
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
|
-
console.log(container.
|
|
26
|
+
console.log(container.introspectAsText('methods'))
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
|
-
console.log(container.
|
|
30
|
+
console.log(container.introspectAsText('getters'))
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Querying Registries
|
|
@@ -60,6 +60,16 @@ console.log(`${allFeatureDocs.length} features documented`)
|
|
|
60
60
|
console.log(allFeatureDocs[0].slice(0, 200) + '...')
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Helper Introspection — Quick Discovery
|
|
64
|
+
|
|
65
|
+
Every helper instance exposes `$methods` and `$getters` for a quick look at what's available:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const git = container.feature('git')
|
|
69
|
+
console.log('methods:', git.$methods)
|
|
70
|
+
console.log('getters:', git.$getters)
|
|
71
|
+
```
|
|
72
|
+
|
|
63
73
|
## Helper Introspection — Structured Data
|
|
64
74
|
|
|
65
75
|
Every helper instance (feature, client, server, etc.) can introspect itself. The result is a typed object you can traverse programmatically.
|
|
@@ -126,7 +136,7 @@ const git = container.feature('git')
|
|
|
126
136
|
console.log(git.introspectAsText('methods', 3)) // headings start at ###
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
##
|
|
139
|
+
## Introspecting State and Options
|
|
130
140
|
|
|
131
141
|
Features that declare Zod schemas for state and options expose them through introspection:
|
|
132
142
|
|
|
@@ -101,8 +101,8 @@ fs.introspectAsText()
|
|
|
101
101
|
The container itself is introspectable:
|
|
102
102
|
|
|
103
103
|
```js
|
|
104
|
-
container.
|
|
105
|
-
container.
|
|
104
|
+
container.introspect() // structured object with all registries, state, events
|
|
105
|
+
container.introspectAsText() // full markdown overview
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
## The REPL
|
|
@@ -137,7 +137,7 @@ luca eval "grep.search('.', 'TODO')"
|
|
|
137
137
|
| Structured introspection? | `feature.introspect()` |
|
|
138
138
|
| What state does it have? | `feature.state.current` |
|
|
139
139
|
| What events does it emit? | `feature.introspect().events` |
|
|
140
|
-
| Full container overview? | `container.
|
|
140
|
+
| Full container overview? | `container.introspectAsText()` |
|
|
141
141
|
| CLI docs for a helper? | `luca describe <name>` |
|
|
142
142
|
|
|
143
143
|
## Gotchas
|
|
@@ -201,10 +201,10 @@ Discover everything about the container at runtime:
|
|
|
201
201
|
|
|
202
202
|
```typescript
|
|
203
203
|
// Structured introspection data
|
|
204
|
-
const info = container.
|
|
204
|
+
const info = container.introspect()
|
|
205
205
|
|
|
206
206
|
// Human-readable markdown
|
|
207
|
-
const docs = container.
|
|
207
|
+
const docs = container.introspectAsText()
|
|
208
208
|
```
|
|
209
209
|
|
|
210
210
|
This is what makes Luca especially powerful for AI agents -- they can discover the entire API surface at runtime without reading documentation.
|
|
@@ -187,6 +187,11 @@ Then anyone (human or AI) can discover your feature:
|
|
|
187
187
|
```typescript
|
|
188
188
|
container.features.describe('sessionManager')
|
|
189
189
|
// Returns the full markdown documentation extracted from your JSDoc
|
|
190
|
+
|
|
191
|
+
// Quick discovery — list available methods and getters
|
|
192
|
+
const session = container.feature('sessionManager')
|
|
193
|
+
session.$methods // => ['createSession', ...]
|
|
194
|
+
session.$getters // => ['activeCount', ...]
|
|
190
195
|
```
|
|
191
196
|
|
|
192
197
|
## Best Practices
|
|
@@ -18,11 +18,11 @@ Introspection serves two audiences:
|
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
20
|
// Structured data about the entire container
|
|
21
|
-
const info = container.
|
|
21
|
+
const info = container.introspect()
|
|
22
22
|
// Returns: registries, enabled features, state schema, available helpers
|
|
23
23
|
|
|
24
24
|
// Human-readable markdown
|
|
25
|
-
const docs = container.
|
|
25
|
+
const docs = container.introspectAsText()
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## Registry-Level Discovery
|
|
@@ -73,6 +73,16 @@ const info = fs.introspect()
|
|
|
73
73
|
const docs = fs.introspectAsText()
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### Quick Discovery with $getters and $methods
|
|
77
|
+
|
|
78
|
+
Every helper exposes `$getters` and `$methods` — string arrays listing what's available on the instance. Useful for quick exploration without parsing the full introspection object:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const fs = container.feature('fs')
|
|
82
|
+
fs.$methods // => ['readFile', 'writeFile', 'walk', 'readdir', ...]
|
|
83
|
+
fs.$getters // => ['cwd', 'sep', ...]
|
|
84
|
+
```
|
|
85
|
+
|
|
76
86
|
### What's in the Introspection Data?
|
|
77
87
|
|
|
78
88
|
- **Class name** and description (from JSDoc)
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Working with Python Projects
|
|
3
|
+
tags: [python, sessions, persistent, bridge, codebase, interop, data-science]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Working with Python Projects
|
|
7
|
+
|
|
8
|
+
Luca's `python` feature has two modes: **stateless** execution (fire-and-forget, one process per call) and **persistent sessions** (a long-lived Python process that maintains state across calls). This tutorial focuses on sessions — the mode that lets you actually work inside a Python codebase.
|
|
9
|
+
|
|
10
|
+
## When to Use Sessions
|
|
11
|
+
|
|
12
|
+
Stateless `execute()` is fine for one-off scripts. But if you need any of these, you want a session:
|
|
13
|
+
|
|
14
|
+
- **Imports that persist** — load `pandas` once, use it across many calls
|
|
15
|
+
- **State that builds up** — query a database, filter results, then export
|
|
16
|
+
- **Working inside a real project** — import your own modules, call your own functions
|
|
17
|
+
- **Expensive setup** — ML model loading, database connections, API client initialization
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```ts skip
|
|
22
|
+
const python = container.feature('python', { dir: '/path/to/my-python-project' })
|
|
23
|
+
await python.enable()
|
|
24
|
+
await python.startSession()
|
|
25
|
+
|
|
26
|
+
// Everything below runs in the same Python process.
|
|
27
|
+
// Variables, imports, and state persist across calls.
|
|
28
|
+
|
|
29
|
+
await python.run('import pandas as pd')
|
|
30
|
+
await python.run('df = pd.read_csv("data/sales.csv")')
|
|
31
|
+
|
|
32
|
+
const result = await python.run('print(df.shape)')
|
|
33
|
+
console.log(result.stdout) // '(1000, 12)\n'
|
|
34
|
+
|
|
35
|
+
const total = await python.eval('df["revenue"].sum()')
|
|
36
|
+
console.log('Total revenue:', total)
|
|
37
|
+
|
|
38
|
+
await python.stopSession()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Project Directory
|
|
42
|
+
|
|
43
|
+
The `dir` option tells Luca where the Python project lives. This determines:
|
|
44
|
+
|
|
45
|
+
1. **sys.path** — the bridge adds the project root (and `src/`, `lib/` if they exist) so your imports work
|
|
46
|
+
2. **Environment detection** — Luca looks for `uv.lock`, `pyproject.toml`, `venv/`, etc. in this directory
|
|
47
|
+
3. **Working directory** — the bridge process runs with `cwd` set to this path
|
|
48
|
+
|
|
49
|
+
```ts skip
|
|
50
|
+
// Explicit project directory
|
|
51
|
+
const python = container.feature('python', { dir: '/Users/me/projects/my-api' })
|
|
52
|
+
|
|
53
|
+
// Or defaults to wherever luca was invoked from
|
|
54
|
+
const python = container.feature('python')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If your project uses a `src/` layout (common in modern Python), the bridge automatically adds it to `sys.path`:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
my-project/
|
|
61
|
+
src/
|
|
62
|
+
myapp/
|
|
63
|
+
__init__.py
|
|
64
|
+
models.py
|
|
65
|
+
pyproject.toml
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ts skip
|
|
69
|
+
await python.startSession()
|
|
70
|
+
// This works because src/ was added to sys.path
|
|
71
|
+
await python.importModule('myapp.models', 'models')
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Session Lifecycle
|
|
75
|
+
|
|
76
|
+
### Starting
|
|
77
|
+
|
|
78
|
+
`startSession()` spawns a Python bridge process that talks to Luca over stdin/stdout using a JSON-line protocol. The bridge sets up `sys.path` and signals when it's ready.
|
|
79
|
+
|
|
80
|
+
```ts skip
|
|
81
|
+
await python.enable()
|
|
82
|
+
await python.startSession()
|
|
83
|
+
|
|
84
|
+
console.log(python.state.get('sessionActive')) // true
|
|
85
|
+
console.log(python.state.get('sessionId')) // uuid
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Stopping
|
|
89
|
+
|
|
90
|
+
`stopSession()` kills the bridge process and cleans up. Any pending requests are rejected.
|
|
91
|
+
|
|
92
|
+
```ts skip
|
|
93
|
+
await python.stopSession()
|
|
94
|
+
console.log(python.state.get('sessionActive')) // false
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Crash Recovery
|
|
98
|
+
|
|
99
|
+
If the Python process dies unexpectedly (segfault, killed externally), the feature:
|
|
100
|
+
- Sets `sessionActive` to `false`
|
|
101
|
+
- Rejects all pending requests
|
|
102
|
+
- Emits a `sessionError` event
|
|
103
|
+
|
|
104
|
+
```ts skip
|
|
105
|
+
python.on('sessionError', ({ error, sessionId }) => {
|
|
106
|
+
console.error('Python session error:', error)
|
|
107
|
+
// You could restart: await python.startSession()
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## The Session API
|
|
112
|
+
|
|
113
|
+
### run(code, variables?)
|
|
114
|
+
|
|
115
|
+
Execute Python code in the persistent namespace. This is the workhorse method.
|
|
116
|
+
|
|
117
|
+
```ts skip
|
|
118
|
+
// Simple execution
|
|
119
|
+
const result = await python.run('print("hello")')
|
|
120
|
+
// result.ok === true
|
|
121
|
+
// result.stdout === 'hello\n'
|
|
122
|
+
|
|
123
|
+
// With variable injection
|
|
124
|
+
const result = await python.run('print(f"Processing {count} items")', { count: 42 })
|
|
125
|
+
|
|
126
|
+
// Errors don't crash the session
|
|
127
|
+
const bad = await python.run('raise ValueError("oops")')
|
|
128
|
+
// bad.ok === false
|
|
129
|
+
// bad.error === 'oops'
|
|
130
|
+
// bad.traceback === 'Traceback (most recent call last):\n...'
|
|
131
|
+
|
|
132
|
+
// Session still alive after error
|
|
133
|
+
const good = await python.run('print("still here")')
|
|
134
|
+
// good.ok === true
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### eval(expression)
|
|
138
|
+
|
|
139
|
+
Evaluate a Python expression and return its value to JavaScript.
|
|
140
|
+
|
|
141
|
+
```ts skip
|
|
142
|
+
await python.run('x = [1, 2, 3]')
|
|
143
|
+
const length = await python.eval('len(x)') // 3
|
|
144
|
+
const doubled = await python.eval('[i*2 for i in x]') // [2, 4, 6]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Values are JSON-serialized. Complex types that can't be serialized come back as their `repr()` string.
|
|
148
|
+
|
|
149
|
+
### importModule(name, alias?)
|
|
150
|
+
|
|
151
|
+
Import a module into the session namespace. The alias defaults to the last segment of the module path.
|
|
152
|
+
|
|
153
|
+
```ts skip
|
|
154
|
+
await python.importModule('json') // import json
|
|
155
|
+
await python.importModule('myapp.models', 'models') // import myapp.models as models
|
|
156
|
+
await python.importModule('os.path') // import os.path (available as "path")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### call(funcPath, args?, kwargs?)
|
|
160
|
+
|
|
161
|
+
Call a function by its dotted path in the namespace.
|
|
162
|
+
|
|
163
|
+
```ts skip
|
|
164
|
+
await python.importModule('json')
|
|
165
|
+
const encoded = await python.call('json.dumps', [{ a: 1 }], { indent: 2 })
|
|
166
|
+
// '{\n "a": 1\n}'
|
|
167
|
+
|
|
168
|
+
// Works with your own functions too
|
|
169
|
+
await python.run('def add(a, b): return a + b')
|
|
170
|
+
const sum = await python.call('add', [3, 4]) // 7
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### getLocals()
|
|
174
|
+
|
|
175
|
+
Inspect everything in the session namespace.
|
|
176
|
+
|
|
177
|
+
```ts skip
|
|
178
|
+
await python.run('x = 42')
|
|
179
|
+
await python.importModule('json')
|
|
180
|
+
const locals = await python.getLocals()
|
|
181
|
+
// { x: 42, json: '<module ...>' }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### resetSession()
|
|
185
|
+
|
|
186
|
+
Clear all variables and imports without restarting the process.
|
|
187
|
+
|
|
188
|
+
```ts skip
|
|
189
|
+
await python.run('big_model = load_model()')
|
|
190
|
+
await python.resetSession()
|
|
191
|
+
// big_model is gone, but the session process is still running
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Real-World Patterns
|
|
195
|
+
|
|
196
|
+
### Data Analysis Pipeline
|
|
197
|
+
|
|
198
|
+
```ts skip
|
|
199
|
+
const python = container.feature('python', { dir: '/path/to/analytics' })
|
|
200
|
+
await python.enable()
|
|
201
|
+
await python.startSession()
|
|
202
|
+
|
|
203
|
+
// Setup
|
|
204
|
+
await python.run('import pandas as pd')
|
|
205
|
+
await python.run('import matplotlib')
|
|
206
|
+
await python.run('matplotlib.use("Agg")') // headless
|
|
207
|
+
await python.run('import matplotlib.pyplot as plt')
|
|
208
|
+
|
|
209
|
+
// Load and analyze
|
|
210
|
+
await python.run('df = pd.read_csv("data/events.csv")')
|
|
211
|
+
const shape = await python.eval('list(df.shape)')
|
|
212
|
+
console.log(`Loaded ${shape[0]} rows, ${shape[1]} columns`)
|
|
213
|
+
|
|
214
|
+
const columns = await python.eval('list(df.columns)')
|
|
215
|
+
console.log('Columns:', columns)
|
|
216
|
+
|
|
217
|
+
// Filter and aggregate
|
|
218
|
+
await python.run(`
|
|
219
|
+
filtered = df[df["status"] == "completed"]
|
|
220
|
+
summary = filtered.groupby("category")["amount"].agg(["sum", "mean", "count"])
|
|
221
|
+
`)
|
|
222
|
+
|
|
223
|
+
const summary = await python.eval('summary.to_dict()')
|
|
224
|
+
console.log('Summary:', summary)
|
|
225
|
+
|
|
226
|
+
// Generate a chart
|
|
227
|
+
await python.run(`
|
|
228
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
229
|
+
summary["sum"].plot(kind="bar", ax=ax)
|
|
230
|
+
ax.set_title("Revenue by Category")
|
|
231
|
+
fig.savefig("output/revenue.png", dpi=150, bbox_inches="tight")
|
|
232
|
+
plt.close(fig)
|
|
233
|
+
`)
|
|
234
|
+
|
|
235
|
+
await python.stopSession()
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Working with a Django Project
|
|
239
|
+
|
|
240
|
+
```ts skip
|
|
241
|
+
const python = container.feature('python', { dir: '/path/to/django-project' })
|
|
242
|
+
await python.enable()
|
|
243
|
+
await python.startSession()
|
|
244
|
+
|
|
245
|
+
// Django requires this before you can import models
|
|
246
|
+
await python.run(`
|
|
247
|
+
import os
|
|
248
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
|
|
249
|
+
|
|
250
|
+
import django
|
|
251
|
+
django.setup()
|
|
252
|
+
`)
|
|
253
|
+
|
|
254
|
+
// Now you can work with the ORM
|
|
255
|
+
await python.run('from myapp.models import User, Order')
|
|
256
|
+
|
|
257
|
+
const userCount = await python.eval('User.objects.count()')
|
|
258
|
+
console.log(`${userCount} users in database`)
|
|
259
|
+
|
|
260
|
+
const recentOrders = await python.eval(`
|
|
261
|
+
list(Order.objects.filter(status="pending").values("id", "total", "created_at")[:10])
|
|
262
|
+
`)
|
|
263
|
+
console.log('Recent pending orders:', recentOrders)
|
|
264
|
+
|
|
265
|
+
await python.stopSession()
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### ML Model Interaction
|
|
269
|
+
|
|
270
|
+
```ts skip
|
|
271
|
+
const python = container.feature('python', { dir: '/path/to/ml-project' })
|
|
272
|
+
await python.enable()
|
|
273
|
+
await python.startSession()
|
|
274
|
+
|
|
275
|
+
// Expensive setup — only happens once
|
|
276
|
+
await python.run(`
|
|
277
|
+
from transformers import pipeline
|
|
278
|
+
classifier = pipeline("sentiment-analysis")
|
|
279
|
+
print("Model loaded")
|
|
280
|
+
`)
|
|
281
|
+
|
|
282
|
+
// Now you can call it cheaply many times
|
|
283
|
+
async function classify(text: string) {
|
|
284
|
+
return python.call('classifier', [text])
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const results = await Promise.all([
|
|
288
|
+
classify('I love this product!'),
|
|
289
|
+
classify('Terrible experience.'),
|
|
290
|
+
classify('It was okay, nothing special.'),
|
|
291
|
+
])
|
|
292
|
+
|
|
293
|
+
console.log(results)
|
|
294
|
+
// [
|
|
295
|
+
// [{ label: 'POSITIVE', score: 0.9998 }],
|
|
296
|
+
// [{ label: 'NEGATIVE', score: 0.9994 }],
|
|
297
|
+
// [{ label: 'NEGATIVE', score: 0.7231 }],
|
|
298
|
+
// ]
|
|
299
|
+
|
|
300
|
+
await python.stopSession()
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Luca Command That Uses Python
|
|
304
|
+
|
|
305
|
+
```ts skip
|
|
306
|
+
// commands/analyze.ts
|
|
307
|
+
import { z } from 'zod'
|
|
308
|
+
import type { ContainerContext } from '@soederpop/luca'
|
|
309
|
+
import { CommandOptionsSchema } from '@soederpop/luca/schemas'
|
|
310
|
+
|
|
311
|
+
export const positionals = ['target']
|
|
312
|
+
export const argsSchema = CommandOptionsSchema.extend({
|
|
313
|
+
target: z.string().describe('Path to CSV file to analyze'),
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
async function handler(options: z.infer<typeof argsSchema>, context: ContainerContext) {
|
|
317
|
+
const container = context.container as any
|
|
318
|
+
const python = container.feature('python')
|
|
319
|
+
await python.enable()
|
|
320
|
+
await python.startSession()
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
await python.run('import pandas as pd')
|
|
324
|
+
await python.run(`df = pd.read_csv("${options.target}")`)
|
|
325
|
+
|
|
326
|
+
const shape = await python.eval('list(df.shape)')
|
|
327
|
+
const dtypes = await python.eval('dict(df.dtypes.astype(str))')
|
|
328
|
+
const nulls = await python.eval('dict(df.isnull().sum())')
|
|
329
|
+
|
|
330
|
+
console.log(`Rows: ${shape[0]}, Columns: ${shape[1]}`)
|
|
331
|
+
console.log('Column types:', dtypes)
|
|
332
|
+
console.log('Null counts:', nulls)
|
|
333
|
+
} finally {
|
|
334
|
+
await python.stopSession()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export default {
|
|
339
|
+
description: 'Analyze a CSV file using pandas',
|
|
340
|
+
argsSchema,
|
|
341
|
+
handler,
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
luca analyze data/sales.csv
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Stateless vs. Session: Choosing the Right Mode
|
|
350
|
+
|
|
351
|
+
| | `execute()` (stateless) | `run()` (session) |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| Process | Fresh per call | Shared, long-lived |
|
|
354
|
+
| State | None — each call starts clean | Persists across calls |
|
|
355
|
+
| Imports | Re-imported every time | Imported once, reused |
|
|
356
|
+
| Startup cost | ~50-200ms per call | ~200ms once, then ~1ms per call |
|
|
357
|
+
| Use case | One-off scripts, simple eval | Real projects, data pipelines, REPL-like |
|
|
358
|
+
| Error isolation | Perfect — crash is contained | Errors caught, session survives |
|
|
359
|
+
|
|
360
|
+
Both modes use the same environment detection (uv, conda, venv, system) and respect the same `dir` and `pythonPath` options.
|
|
361
|
+
|
|
362
|
+
## Environment Detection
|
|
363
|
+
|
|
364
|
+
The feature detects Python environments in this order:
|
|
365
|
+
|
|
366
|
+
1. **Explicit** — `pythonPath` option overrides everything
|
|
367
|
+
2. **uv** — `uv.lock` or `pyproject.toml` present, `uv run python` works
|
|
368
|
+
3. **conda** — `environment.yml` or `conda.yml` present
|
|
369
|
+
4. **venv** — `venv/` or `.venv/` directory with a Python binary inside
|
|
370
|
+
5. **system** — falls back to `python3` or `python` on PATH
|
|
371
|
+
|
|
372
|
+
```ts skip
|
|
373
|
+
const python = container.feature('python', { dir: '/path/to/project' })
|
|
374
|
+
await python.enable()
|
|
375
|
+
console.log(python.environmentType) // 'uv' | 'conda' | 'venv' | 'system'
|
|
376
|
+
console.log(python.pythonPath) // e.g. '/Users/me/.local/bin/uv run python'
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Events
|
|
380
|
+
|
|
381
|
+
The session emits events you can listen to for monitoring and debugging:
|
|
382
|
+
|
|
383
|
+
```ts skip
|
|
384
|
+
python.on('sessionStarted', ({ sessionId }) => {
|
|
385
|
+
console.log('Session started:', sessionId)
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
python.on('sessionStopped', ({ sessionId }) => {
|
|
389
|
+
console.log('Session stopped:', sessionId)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
python.on('sessionError', ({ error, sessionId }) => {
|
|
393
|
+
console.error('Session error:', error)
|
|
394
|
+
})
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## What's Next
|
|
398
|
+
|
|
399
|
+
- [Creating Features](./10-creating-features.md) — build your own feature that wraps a Python service
|
|
400
|
+
- [Commands](./08-commands.md) — create CLI commands that leverage Python
|
|
401
|
+
- [Servers and Endpoints](./06-servers.md) — expose Python-powered analysis via HTTP
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soederpop/luca",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"website": "https://luca.soederpop.com",
|
|
5
5
|
"description": "lightweight universal conversational architecture AKA Le Ultimate Component Architecture AKA Last Universal Common Ancestor, part AI part Human",
|
|
6
6
|
"author": "jon soeder aka the people's champ <jon@soederpop.com>",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"./container": "./src/container.ts",
|
|
35
35
|
"./client": "./src/client.ts",
|
|
36
36
|
"./clients/*": "./src/clients/*",
|
|
37
|
+
"./server": "./src/server.ts",
|
|
38
|
+
"./servers/*": "./src/servers/*",
|
|
37
39
|
"./feature": "./src/feature.ts",
|
|
38
40
|
"./react": "./src/react/index.ts",
|
|
39
41
|
"./introspection": "./src/introspection/index.ts"
|
|
@@ -48,13 +50,14 @@
|
|
|
48
50
|
"clean": "rm -rf dist build package",
|
|
49
51
|
"build:web": "bun run scripts/build-web.ts",
|
|
50
52
|
"build:introspection": "bun run src/cli/cli.ts introspect",
|
|
51
|
-
"compile": "bun run build:introspection && bun run build:scaffolds && bun run build:bootstrap && bash scripts/stamp-build.sh && bun build ./src/cli/cli.ts --compile --outfile dist/luca --external node-llama-cpp",
|
|
53
|
+
"compile": "bun run build:introspection && bun run build:scaffolds && bun run build:bootstrap && bun run build:python-bridge && bash scripts/stamp-build.sh && bun build ./src/cli/cli.ts --compile --outfile dist/luca --external node-llama-cpp",
|
|
52
54
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
55
|
"build:scaffolds": "bun run src/cli/cli.ts build-scaffolds",
|
|
54
56
|
"build:bootstrap": "bun run src/cli/cli.ts build-bootstrap",
|
|
57
|
+
"build:python-bridge": "bun run src/cli/cli.ts build-python-bridge",
|
|
55
58
|
"test": "bun test test/*.test.ts",
|
|
56
|
-
"update-all-docs": "bun run test && bun run build:introspection && bun run src/cli/cli.ts generate-api-docs && bun run build:scaffolds && bun run build:bootstrap",
|
|
57
|
-
"precommit": "bun run update-all-docs && git add docs/apis/ src/introspection/generated.*.ts src/scaffolds/generated.ts src/bootstrap/generated.ts && bun compile",
|
|
59
|
+
"update-all-docs": "bun run test && bun run build:introspection && bun run src/cli/cli.ts generate-api-docs && bun run build:scaffolds && bun run build:bootstrap && bun run build:python-bridge",
|
|
60
|
+
"precommit": "bun run update-all-docs && git add docs/apis/ src/introspection/generated.*.ts src/scaffolds/generated.ts src/bootstrap/generated.ts src/python/generated.ts && bun compile",
|
|
58
61
|
"test:integration": "bun test ./test-integration/"
|
|
59
62
|
},
|
|
60
63
|
"devDependencies": {
|
|
@@ -117,6 +120,7 @@
|
|
|
117
120
|
"inflect": "^0.5.0",
|
|
118
121
|
"ink": "^6.7.0",
|
|
119
122
|
"inquirer": "^9.1.5",
|
|
123
|
+
"ioredis": "^5.10.1",
|
|
120
124
|
"isomorphic-vm": "^0.0.1",
|
|
121
125
|
"isomorphic-ws": "^5.0.0",
|
|
122
126
|
"js-tiktoken": "^1.0.21",
|
|
@@ -14,6 +14,8 @@ import { SkillsLibrary } from './features/skills-library'
|
|
|
14
14
|
import { BrowserUse } from './features/browser-use'
|
|
15
15
|
import { SemanticSearch } from '@soederpop/luca/node/features/semantic-search'
|
|
16
16
|
import { ContentDb } from '@soederpop/luca/node/features/content-db'
|
|
17
|
+
import { FileTools } from './features/file-tools'
|
|
18
|
+
import { LucaCoder } from './features/luca-coder'
|
|
17
19
|
|
|
18
20
|
import type { ConversationTool } from './features/conversation'
|
|
19
21
|
import type { ZodType } from 'zod'
|
|
@@ -28,6 +30,8 @@ export {
|
|
|
28
30
|
DocsReader,
|
|
29
31
|
SkillsLibrary,
|
|
30
32
|
BrowserUse,
|
|
33
|
+
FileTools,
|
|
34
|
+
LucaCoder,
|
|
31
35
|
SemanticSearch,
|
|
32
36
|
ContentDb,
|
|
33
37
|
NodeContainer,
|
|
@@ -52,6 +56,8 @@ export interface AGIFeatures extends NodeFeatures {
|
|
|
52
56
|
docsReader: typeof DocsReader
|
|
53
57
|
skillsLibrary: typeof SkillsLibrary
|
|
54
58
|
browserUse: typeof BrowserUse
|
|
59
|
+
fileTools: typeof FileTools
|
|
60
|
+
lucaCoder: typeof LucaCoder
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export interface ConversationFactoryOptions {
|
|
@@ -125,6 +131,8 @@ const container = new AGIContainer()
|
|
|
125
131
|
.use(DocsReader)
|
|
126
132
|
.use(SkillsLibrary)
|
|
127
133
|
.use(BrowserUse)
|
|
134
|
+
.use(FileTools)
|
|
135
|
+
.use(LucaCoder)
|
|
128
136
|
.use(SemanticSearch)
|
|
129
137
|
|
|
130
138
|
container.docs = container.feature('contentDb', {
|
|
@@ -79,6 +79,18 @@ export const AssistantOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
79
79
|
/** Maximum number of output tokens per completion */
|
|
80
80
|
|
|
81
81
|
maxTokens: z.number().optional().describe('Maximum number of output tokens per completion'),
|
|
82
|
+
/** Sampling temperature (0-2). Higher = more random, lower = more deterministic. */
|
|
83
|
+
temperature: z.number().min(0).max(2).optional().describe('Sampling temperature (0-2)'),
|
|
84
|
+
/** Nucleus sampling cutoff (0-1). */
|
|
85
|
+
topP: z.number().min(0).max(1).optional().describe('Nucleus sampling cutoff (0-1)'),
|
|
86
|
+
/** Top-K sampling. Only supported by local/Anthropic models. */
|
|
87
|
+
topK: z.number().optional().describe('Top-K sampling. Only supported by local/Anthropic models'),
|
|
88
|
+
/** Frequency penalty (-2 to 2). */
|
|
89
|
+
frequencyPenalty: z.number().min(-2).max(2).optional().describe('Frequency penalty (-2 to 2)'),
|
|
90
|
+
/** Presence penalty (-2 to 2). */
|
|
91
|
+
presencePenalty: z.number().min(-2).max(2).optional().describe('Presence penalty (-2 to 2)'),
|
|
92
|
+
/** Stop sequences. */
|
|
93
|
+
stop: z.array(z.string()).optional().describe('Stop sequences'),
|
|
82
94
|
|
|
83
95
|
local: z.boolean().default(false).describe('Whether to use our local models for this'),
|
|
84
96
|
|
|
@@ -261,6 +273,12 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
261
273
|
tools: this.tools,
|
|
262
274
|
api: 'chat',
|
|
263
275
|
...(this.options.maxTokens ? { maxTokens: this.options.maxTokens } : {}),
|
|
276
|
+
...(this.options.temperature != null ? { temperature: this.options.temperature } : {}),
|
|
277
|
+
...(this.options.topP != null ? { topP: this.options.topP } : {}),
|
|
278
|
+
...(this.options.topK != null ? { topK: this.options.topK } : {}),
|
|
279
|
+
...(this.options.frequencyPenalty != null ? { frequencyPenalty: this.options.frequencyPenalty } : {}),
|
|
280
|
+
...(this.options.presencePenalty != null ? { presencePenalty: this.options.presencePenalty } : {}),
|
|
281
|
+
...(this.options.stop ? { stop: this.options.stop } : {}),
|
|
264
282
|
history: [
|
|
265
283
|
{ role: 'system', content: this.effectiveSystemPrompt },
|
|
266
284
|
],
|
|
@@ -962,6 +980,7 @@ export class Assistant extends Feature<AssistantState, AssistantOptions> {
|
|
|
962
980
|
}
|
|
963
981
|
|
|
964
982
|
private unbindHooksFromEvents() {
|
|
983
|
+
if (!this._boundHookListeners) return
|
|
965
984
|
for (const { event, listener } of this._boundHookListeners) {
|
|
966
985
|
this.off(event as any, listener)
|
|
967
986
|
}
|