@usejarvis/brain 0.1.0
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/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { test, expect, beforeEach, describe } from 'bun:test';
|
|
2
|
+
import { initDatabase } from './schema.ts';
|
|
3
|
+
import {
|
|
4
|
+
buildExtractionPrompt,
|
|
5
|
+
parseExtractionResponse,
|
|
6
|
+
extractAndStore,
|
|
7
|
+
type ExtractionResult,
|
|
8
|
+
} from './extractor.ts';
|
|
9
|
+
import { findEntities } from './entities.ts';
|
|
10
|
+
import { findFacts } from './facts.ts';
|
|
11
|
+
import { findRelationships } from './relationships.ts';
|
|
12
|
+
import { findCommitments } from './commitments.ts';
|
|
13
|
+
import type { LLMProvider, LLMMessage, LLMOptions, LLMResponse } from '../llm/provider.ts';
|
|
14
|
+
|
|
15
|
+
describe('Vault Extractor', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Initialize in-memory database for each test
|
|
18
|
+
initDatabase(':memory:');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('buildExtractionPrompt', () => {
|
|
22
|
+
test('should build prompt with user and assistant messages', () => {
|
|
23
|
+
const userMessage = "My sister Anna's birthday is March 15th";
|
|
24
|
+
const assistantResponse = "I'll remember that Anna's birthday is March 15th!";
|
|
25
|
+
|
|
26
|
+
const prompt = buildExtractionPrompt(userMessage, assistantResponse);
|
|
27
|
+
|
|
28
|
+
expect(prompt).toContain('USER MESSAGE:');
|
|
29
|
+
expect(prompt).toContain(userMessage);
|
|
30
|
+
expect(prompt).toContain('ASSISTANT RESPONSE:');
|
|
31
|
+
expect(prompt).toContain(assistantResponse);
|
|
32
|
+
expect(prompt).toContain('entities');
|
|
33
|
+
expect(prompt).toContain('facts');
|
|
34
|
+
expect(prompt).toContain('relationships');
|
|
35
|
+
expect(prompt).toContain('commitments');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('parseExtractionResponse', () => {
|
|
40
|
+
test('should parse valid JSON response', () => {
|
|
41
|
+
const response = JSON.stringify({
|
|
42
|
+
entities: [
|
|
43
|
+
{ name: 'Anna', type: 'person' },
|
|
44
|
+
],
|
|
45
|
+
facts: [
|
|
46
|
+
{ subject: 'Anna', predicate: 'birthday_is', object: 'March 15', confidence: 1.0 },
|
|
47
|
+
],
|
|
48
|
+
relationships: [],
|
|
49
|
+
commitments: [],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = parseExtractionResponse(response);
|
|
53
|
+
|
|
54
|
+
expect(result.entities).toHaveLength(1);
|
|
55
|
+
expect(result.entities[0]!.name).toBe('Anna');
|
|
56
|
+
expect(result.facts).toHaveLength(1);
|
|
57
|
+
expect(result.facts[0]!.predicate).toBe('birthday_is');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should handle markdown code blocks', () => {
|
|
61
|
+
const response = `\`\`\`json
|
|
62
|
+
{
|
|
63
|
+
"entities": [{ "name": "Bob", "type": "person" }],
|
|
64
|
+
"facts": [],
|
|
65
|
+
"relationships": [],
|
|
66
|
+
"commitments": []
|
|
67
|
+
}
|
|
68
|
+
\`\`\``;
|
|
69
|
+
|
|
70
|
+
const result = parseExtractionResponse(response);
|
|
71
|
+
|
|
72
|
+
expect(result.entities).toHaveLength(1);
|
|
73
|
+
expect(result.entities[0]!.name).toBe('Bob');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should return empty result on invalid JSON', () => {
|
|
77
|
+
const response = 'This is not JSON';
|
|
78
|
+
|
|
79
|
+
const result = parseExtractionResponse(response);
|
|
80
|
+
|
|
81
|
+
expect(result.entities).toHaveLength(0);
|
|
82
|
+
expect(result.facts).toHaveLength(0);
|
|
83
|
+
expect(result.relationships).toHaveLength(0);
|
|
84
|
+
expect(result.commitments).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should handle missing fields', () => {
|
|
88
|
+
const response = JSON.stringify({
|
|
89
|
+
entities: [{ name: 'Test', type: 'person' }],
|
|
90
|
+
// Missing facts, relationships, commitments
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const result = parseExtractionResponse(response);
|
|
94
|
+
|
|
95
|
+
expect(result.entities).toHaveLength(1);
|
|
96
|
+
expect(result.facts).toHaveLength(0);
|
|
97
|
+
expect(result.relationships).toHaveLength(0);
|
|
98
|
+
expect(result.commitments).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('extractAndStore', () => {
|
|
103
|
+
test('should return empty result when no provider given', async () => {
|
|
104
|
+
const result = await extractAndStore('Hello', 'Hi there');
|
|
105
|
+
|
|
106
|
+
expect(result.entities).toHaveLength(0);
|
|
107
|
+
expect(result.facts).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('should extract and store entities', async () => {
|
|
111
|
+
const mockProvider: LLMProvider = {
|
|
112
|
+
name: 'mock',
|
|
113
|
+
async chat(messages: LLMMessage[], options?: LLMOptions): Promise<LLMResponse> {
|
|
114
|
+
return {
|
|
115
|
+
content: JSON.stringify({
|
|
116
|
+
entities: [
|
|
117
|
+
{ name: 'Alice', type: 'person' },
|
|
118
|
+
{ name: 'Project Phoenix', type: 'project' },
|
|
119
|
+
],
|
|
120
|
+
facts: [],
|
|
121
|
+
relationships: [],
|
|
122
|
+
commitments: [],
|
|
123
|
+
}),
|
|
124
|
+
tool_calls: [],
|
|
125
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
126
|
+
model: 'mock-model',
|
|
127
|
+
finish_reason: 'stop',
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
async *stream() {
|
|
131
|
+
yield { type: 'done', response: {} as any };
|
|
132
|
+
},
|
|
133
|
+
async listModels() {
|
|
134
|
+
return ['mock-model'];
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = await extractAndStore(
|
|
139
|
+
'Alice is working on Project Phoenix',
|
|
140
|
+
'Got it!',
|
|
141
|
+
mockProvider
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(result.entities).toHaveLength(2);
|
|
145
|
+
|
|
146
|
+
// Verify entities were stored in database
|
|
147
|
+
const entities = findEntities({});
|
|
148
|
+
expect(entities).toHaveLength(2);
|
|
149
|
+
expect(entities.find((e) => e.name === 'Alice')).toBeDefined();
|
|
150
|
+
expect(entities.find((e) => e.name === 'Project Phoenix')).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should extract and store facts', async () => {
|
|
154
|
+
const mockProvider: LLMProvider = {
|
|
155
|
+
name: 'mock',
|
|
156
|
+
async chat(): Promise<LLMResponse> {
|
|
157
|
+
return {
|
|
158
|
+
content: JSON.stringify({
|
|
159
|
+
entities: [
|
|
160
|
+
{ name: 'Bob', type: 'person' },
|
|
161
|
+
],
|
|
162
|
+
facts: [
|
|
163
|
+
{ subject: 'Bob', predicate: 'email_is', object: 'bob@example.com', confidence: 1.0 },
|
|
164
|
+
],
|
|
165
|
+
relationships: [],
|
|
166
|
+
commitments: [],
|
|
167
|
+
}),
|
|
168
|
+
tool_calls: [],
|
|
169
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
170
|
+
model: 'mock-model',
|
|
171
|
+
finish_reason: 'stop',
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
async *stream() {
|
|
175
|
+
yield { type: 'done', response: {} as any };
|
|
176
|
+
},
|
|
177
|
+
async listModels() {
|
|
178
|
+
return ['mock-model'];
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
await extractAndStore(
|
|
183
|
+
"Bob's email is bob@example.com",
|
|
184
|
+
'Noted!',
|
|
185
|
+
mockProvider
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Verify fact was stored
|
|
189
|
+
const facts = findFacts({});
|
|
190
|
+
expect(facts).toHaveLength(1);
|
|
191
|
+
expect(facts[0]!.predicate).toBe('email_is');
|
|
192
|
+
expect(facts[0]!.object).toBe('bob@example.com');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('should extract and store relationships', async () => {
|
|
196
|
+
const mockProvider: LLMProvider = {
|
|
197
|
+
name: 'mock',
|
|
198
|
+
async chat(): Promise<LLMResponse> {
|
|
199
|
+
return {
|
|
200
|
+
content: JSON.stringify({
|
|
201
|
+
entities: [
|
|
202
|
+
{ name: 'Alice', type: 'person' },
|
|
203
|
+
{ name: 'Bob', type: 'person' },
|
|
204
|
+
],
|
|
205
|
+
facts: [],
|
|
206
|
+
relationships: [
|
|
207
|
+
{ from: 'Alice', to: 'Bob', type: 'manages' },
|
|
208
|
+
],
|
|
209
|
+
commitments: [],
|
|
210
|
+
}),
|
|
211
|
+
tool_calls: [],
|
|
212
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
213
|
+
model: 'mock-model',
|
|
214
|
+
finish_reason: 'stop',
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
async *stream() {
|
|
218
|
+
yield { type: 'done', response: {} as any };
|
|
219
|
+
},
|
|
220
|
+
async listModels() {
|
|
221
|
+
return ['mock-model'];
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
await extractAndStore(
|
|
226
|
+
'Alice manages Bob',
|
|
227
|
+
'Understood!',
|
|
228
|
+
mockProvider
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Verify relationship was stored
|
|
232
|
+
const relationships = findRelationships({});
|
|
233
|
+
expect(relationships).toHaveLength(1);
|
|
234
|
+
expect(relationships[0]!.type).toBe('manages');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('should extract and store commitments', async () => {
|
|
238
|
+
const mockProvider: LLMProvider = {
|
|
239
|
+
name: 'mock',
|
|
240
|
+
async chat(): Promise<LLMResponse> {
|
|
241
|
+
return {
|
|
242
|
+
content: JSON.stringify({
|
|
243
|
+
entities: [],
|
|
244
|
+
facts: [],
|
|
245
|
+
relationships: [],
|
|
246
|
+
commitments: [
|
|
247
|
+
{
|
|
248
|
+
what: 'Remind about meeting',
|
|
249
|
+
when_due: '2026-03-15T10:00:00Z',
|
|
250
|
+
priority: 'high',
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
}),
|
|
254
|
+
tool_calls: [],
|
|
255
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
256
|
+
model: 'mock-model',
|
|
257
|
+
finish_reason: 'stop',
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
async *stream() {
|
|
261
|
+
yield { type: 'done', response: {} as any };
|
|
262
|
+
},
|
|
263
|
+
async listModels() {
|
|
264
|
+
return ['mock-model'];
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await extractAndStore(
|
|
269
|
+
'Remind me about the meeting on March 15',
|
|
270
|
+
'Will do!',
|
|
271
|
+
mockProvider
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Verify commitment was stored
|
|
275
|
+
const commitments = findCommitments({});
|
|
276
|
+
expect(commitments).toHaveLength(1);
|
|
277
|
+
expect(commitments[0]!.what).toBe('Remind about meeting');
|
|
278
|
+
expect(commitments[0]!.priority).toBe('high');
|
|
279
|
+
expect(commitments[0]!.when_due).toBeTruthy();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('should reuse existing entities', async () => {
|
|
283
|
+
const mockProvider: LLMProvider = {
|
|
284
|
+
name: 'mock',
|
|
285
|
+
async chat(): Promise<LLMResponse> {
|
|
286
|
+
return {
|
|
287
|
+
content: JSON.stringify({
|
|
288
|
+
entities: [
|
|
289
|
+
{ name: 'Charlie', type: 'person' },
|
|
290
|
+
],
|
|
291
|
+
facts: [
|
|
292
|
+
{ subject: 'Charlie', predicate: 'location_is', object: 'NYC', confidence: 1.0 },
|
|
293
|
+
],
|
|
294
|
+
relationships: [],
|
|
295
|
+
commitments: [],
|
|
296
|
+
}),
|
|
297
|
+
tool_calls: [],
|
|
298
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
299
|
+
model: 'mock-model',
|
|
300
|
+
finish_reason: 'stop',
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
async *stream() {
|
|
304
|
+
yield { type: 'done', response: {} as any };
|
|
305
|
+
},
|
|
306
|
+
async listModels() {
|
|
307
|
+
return ['mock-model'];
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// First extraction
|
|
312
|
+
await extractAndStore('Charlie lives in NYC', 'Got it!', mockProvider);
|
|
313
|
+
|
|
314
|
+
// Second extraction with same entity
|
|
315
|
+
await extractAndStore('Charlie works remotely', 'Noted!', mockProvider);
|
|
316
|
+
|
|
317
|
+
// Should only have one Charlie entity
|
|
318
|
+
const entities = findEntities({ name: 'Charlie' });
|
|
319
|
+
expect(entities).toHaveLength(1);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('should handle invalid entity types', async () => {
|
|
323
|
+
const mockProvider: LLMProvider = {
|
|
324
|
+
name: 'mock',
|
|
325
|
+
async chat(): Promise<LLMResponse> {
|
|
326
|
+
return {
|
|
327
|
+
content: JSON.stringify({
|
|
328
|
+
entities: [
|
|
329
|
+
{ name: 'Invalid', type: 'invalid_type' },
|
|
330
|
+
],
|
|
331
|
+
facts: [],
|
|
332
|
+
relationships: [],
|
|
333
|
+
commitments: [],
|
|
334
|
+
}),
|
|
335
|
+
tool_calls: [],
|
|
336
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
337
|
+
model: 'mock-model',
|
|
338
|
+
finish_reason: 'stop',
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
async *stream() {
|
|
342
|
+
yield { type: 'done', response: {} as any };
|
|
343
|
+
},
|
|
344
|
+
async listModels() {
|
|
345
|
+
return ['mock-model'];
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
await extractAndStore('Test', 'Test', mockProvider);
|
|
350
|
+
|
|
351
|
+
// Entity should not be created
|
|
352
|
+
const entities = findEntities({});
|
|
353
|
+
expect(entities).toHaveLength(0);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { LLMProvider } from '../llm/provider.ts';
|
|
2
|
+
import { createEntity, findEntities } from './entities.ts';
|
|
3
|
+
import { createFact } from './facts.ts';
|
|
4
|
+
import { createRelationship } from './relationships.ts';
|
|
5
|
+
import { createCommitment } from './commitments.ts';
|
|
6
|
+
|
|
7
|
+
export type ExtractionResult = {
|
|
8
|
+
entities: Array<{ name: string; type: string; properties?: Record<string, unknown> }>;
|
|
9
|
+
facts: Array<{ subject: string; predicate: string; object: string; confidence: number }>;
|
|
10
|
+
relationships: Array<{ from: string; to: string; type: string }>;
|
|
11
|
+
commitments: Array<{ what: string; when_due?: string; priority?: string }>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build extraction prompt for LLM
|
|
16
|
+
*/
|
|
17
|
+
export function buildExtractionPrompt(userMessage: string, assistantResponse: string): string {
|
|
18
|
+
return `You are an expert at extracting structured information from conversations. Analyze the following conversation and extract entities, facts, relationships, and commitments.
|
|
19
|
+
|
|
20
|
+
USER MESSAGE:
|
|
21
|
+
${userMessage}
|
|
22
|
+
|
|
23
|
+
ASSISTANT RESPONSE:
|
|
24
|
+
${assistantResponse}
|
|
25
|
+
|
|
26
|
+
Extract the following information and return ONLY valid JSON (no markdown, no explanation):
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
"entities": [
|
|
30
|
+
{
|
|
31
|
+
"name": "Entity name",
|
|
32
|
+
"type": "person|project|tool|place|concept|event",
|
|
33
|
+
"properties": {}
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"facts": [
|
|
37
|
+
{
|
|
38
|
+
"subject": "Entity name",
|
|
39
|
+
"predicate": "property_name",
|
|
40
|
+
"object": "value",
|
|
41
|
+
"confidence": 0.0-1.0
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"relationships": [
|
|
45
|
+
{
|
|
46
|
+
"from": "Entity A name",
|
|
47
|
+
"to": "Entity B name",
|
|
48
|
+
"type": "relationship_type"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"commitments": [
|
|
52
|
+
{
|
|
53
|
+
"what": "Description of commitment",
|
|
54
|
+
"when_due": "ISO date string (optional)",
|
|
55
|
+
"priority": "low|normal|high|critical (optional)"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
GUIDELINES:
|
|
61
|
+
- Extract only concrete, verifiable information
|
|
62
|
+
- For entities: identify people, projects, tools, places, concepts, events
|
|
63
|
+
- For facts: extract attributes about entities (e.g., "birthday_is", "works_at", "location_is")
|
|
64
|
+
- For relationships: extract connections between entities (e.g., "sister_of", "manages", "part_of")
|
|
65
|
+
- For commitments: extract any promises, tasks, or reminders mentioned
|
|
66
|
+
- Use snake_case for predicates and relationship types
|
|
67
|
+
- Set confidence lower (0.5-0.8) for implied or uncertain information
|
|
68
|
+
- If no information to extract, return empty arrays
|
|
69
|
+
- Respond with ONLY the JSON object, no other text`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse LLM response into ExtractionResult
|
|
74
|
+
*/
|
|
75
|
+
export function parseExtractionResponse(llmResponse: string): ExtractionResult {
|
|
76
|
+
// Clean up response - remove markdown code blocks if present
|
|
77
|
+
let cleaned = llmResponse.trim();
|
|
78
|
+
|
|
79
|
+
// Remove markdown JSON code blocks
|
|
80
|
+
if (cleaned.startsWith('```json')) {
|
|
81
|
+
cleaned = cleaned.replace(/^```json\s*/, '').replace(/\s*```$/, '');
|
|
82
|
+
} else if (cleaned.startsWith('```')) {
|
|
83
|
+
cleaned = cleaned.replace(/^```\s*/, '').replace(/\s*```$/, '');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(cleaned);
|
|
88
|
+
|
|
89
|
+
// Validate and normalize the structure
|
|
90
|
+
const result: ExtractionResult = {
|
|
91
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
92
|
+
facts: Array.isArray(parsed.facts) ? parsed.facts : [],
|
|
93
|
+
relationships: Array.isArray(parsed.relationships) ? parsed.relationships : [],
|
|
94
|
+
commitments: Array.isArray(parsed.commitments) ? parsed.commitments : [],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
} catch (_error) {
|
|
99
|
+
|
|
100
|
+
// Return empty result on parse failure
|
|
101
|
+
return {
|
|
102
|
+
entities: [],
|
|
103
|
+
facts: [],
|
|
104
|
+
relationships: [],
|
|
105
|
+
commitments: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse ISO date string to timestamp, return null if invalid
|
|
112
|
+
*/
|
|
113
|
+
function parseDate(dateStr?: string): number | null {
|
|
114
|
+
if (!dateStr) return null;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const timestamp = new Date(dateStr).getTime();
|
|
118
|
+
return isNaN(timestamp) ? null : timestamp;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validate entity type
|
|
126
|
+
*/
|
|
127
|
+
function isValidEntityType(type: string): type is 'person' | 'project' | 'tool' | 'place' | 'concept' | 'event' {
|
|
128
|
+
return ['person', 'project', 'tool', 'place', 'concept', 'event'].includes(type);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* High-level: extract and store in vault
|
|
133
|
+
*/
|
|
134
|
+
export async function extractAndStore(
|
|
135
|
+
userMessage: string,
|
|
136
|
+
assistantResponse: string,
|
|
137
|
+
provider?: LLMProvider
|
|
138
|
+
): Promise<ExtractionResult> {
|
|
139
|
+
// If no provider, return empty result
|
|
140
|
+
if (!provider) {
|
|
141
|
+
return {
|
|
142
|
+
entities: [],
|
|
143
|
+
facts: [],
|
|
144
|
+
relationships: [],
|
|
145
|
+
commitments: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// Build prompt
|
|
151
|
+
const prompt = buildExtractionPrompt(userMessage, assistantResponse);
|
|
152
|
+
|
|
153
|
+
// Call LLM
|
|
154
|
+
const response = await provider.chat([
|
|
155
|
+
{ role: 'user', content: prompt },
|
|
156
|
+
], {
|
|
157
|
+
temperature: 0.1, // Low temperature for consistent extraction
|
|
158
|
+
max_tokens: 2000,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Parse response
|
|
162
|
+
const extraction = parseExtractionResponse(response.content);
|
|
163
|
+
|
|
164
|
+
// Store entities
|
|
165
|
+
const entityMap = new Map<string, string>(); // name -> id
|
|
166
|
+
|
|
167
|
+
for (const entityData of extraction.entities) {
|
|
168
|
+
const { name, type, properties } = entityData;
|
|
169
|
+
|
|
170
|
+
// Validate type
|
|
171
|
+
if (!isValidEntityType(type)) {
|
|
172
|
+
console.warn(`Invalid entity type: ${type}, skipping entity ${name}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if entity already exists
|
|
177
|
+
const existing = findEntities({ name, type });
|
|
178
|
+
|
|
179
|
+
if (existing.length > 0) {
|
|
180
|
+
// Use existing entity ID
|
|
181
|
+
entityMap.set(name, existing[0]!.id);
|
|
182
|
+
} else {
|
|
183
|
+
// Create new entity
|
|
184
|
+
const entity = createEntity(
|
|
185
|
+
type,
|
|
186
|
+
name,
|
|
187
|
+
properties,
|
|
188
|
+
'llm_extraction'
|
|
189
|
+
);
|
|
190
|
+
entityMap.set(name, entity.id);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Store facts
|
|
195
|
+
for (const factData of extraction.facts) {
|
|
196
|
+
const { subject, predicate, object, confidence } = factData;
|
|
197
|
+
|
|
198
|
+
// Get subject entity ID
|
|
199
|
+
const subjectId = entityMap.get(subject);
|
|
200
|
+
if (!subjectId) {
|
|
201
|
+
console.warn(`Subject entity not found: ${subject}, skipping fact`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
createFact(subjectId, predicate, object, {
|
|
206
|
+
confidence: confidence ?? 1.0,
|
|
207
|
+
source: 'llm_extraction',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Store relationships
|
|
212
|
+
for (const relData of extraction.relationships) {
|
|
213
|
+
const { from, to, type } = relData;
|
|
214
|
+
|
|
215
|
+
// Get entity IDs
|
|
216
|
+
const fromId = entityMap.get(from);
|
|
217
|
+
const toId = entityMap.get(to);
|
|
218
|
+
|
|
219
|
+
if (!fromId || !toId) {
|
|
220
|
+
console.warn(`Relationship entities not found: ${from} -> ${to}, skipping`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
createRelationship(fromId, toId, type);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Store commitments
|
|
228
|
+
for (const commitmentData of extraction.commitments) {
|
|
229
|
+
const { what, when_due, priority } = commitmentData;
|
|
230
|
+
|
|
231
|
+
const whenDueTimestamp = parseDate(when_due);
|
|
232
|
+
|
|
233
|
+
createCommitment(what, {
|
|
234
|
+
when_due: whenDueTimestamp ?? undefined,
|
|
235
|
+
priority: (priority as any) ?? 'normal',
|
|
236
|
+
context: `Extracted from conversation`,
|
|
237
|
+
created_from: 'llm_extraction',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return extraction;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error('Failed to extract and store:', error);
|
|
244
|
+
|
|
245
|
+
// Return empty result on error
|
|
246
|
+
return {
|
|
247
|
+
entities: [],
|
|
248
|
+
facts: [],
|
|
249
|
+
relationships: [],
|
|
250
|
+
commitments: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Extract a completed goal as a vault entity with performance facts.
|
|
257
|
+
* Called when a goal is completed/failed/killed to build historical data
|
|
258
|
+
* for future estimation.
|
|
259
|
+
*/
|
|
260
|
+
export function extractGoalCompletion(goal: {
|
|
261
|
+
id: string;
|
|
262
|
+
title: string;
|
|
263
|
+
level: string;
|
|
264
|
+
score: number;
|
|
265
|
+
status: string;
|
|
266
|
+
estimated_hours: number | null;
|
|
267
|
+
actual_hours: number;
|
|
268
|
+
created_at: number;
|
|
269
|
+
completed_at: number | null;
|
|
270
|
+
tags: string[];
|
|
271
|
+
}): void {
|
|
272
|
+
try {
|
|
273
|
+
// Create or find entity for this goal
|
|
274
|
+
const existing = findEntities({ name: goal.title, type: 'concept' });
|
|
275
|
+
let entityId: string;
|
|
276
|
+
|
|
277
|
+
if (existing.length > 0) {
|
|
278
|
+
entityId = existing[0]!.id;
|
|
279
|
+
} else {
|
|
280
|
+
const entity = createEntity('concept', goal.title, {
|
|
281
|
+
goal_id: goal.id,
|
|
282
|
+
goal_level: goal.level,
|
|
283
|
+
}, 'goal_completion');
|
|
284
|
+
entityId = entity.id;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Store performance facts
|
|
288
|
+
createFact(entityId, 'goal_final_score', goal.score.toFixed(2), {
|
|
289
|
+
confidence: 1.0,
|
|
290
|
+
source: 'goal_completion',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
createFact(entityId, 'goal_outcome', goal.status, {
|
|
294
|
+
confidence: 1.0,
|
|
295
|
+
source: 'goal_completion',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
createFact(entityId, 'goal_level', goal.level, {
|
|
299
|
+
confidence: 1.0,
|
|
300
|
+
source: 'goal_completion',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (goal.estimated_hours !== null) {
|
|
304
|
+
createFact(entityId, 'estimated_hours', goal.estimated_hours.toString(), {
|
|
305
|
+
confidence: 1.0,
|
|
306
|
+
source: 'goal_completion',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (goal.actual_hours > 0) {
|
|
311
|
+
createFact(entityId, 'actual_hours', goal.actual_hours.toFixed(1), {
|
|
312
|
+
confidence: 1.0,
|
|
313
|
+
source: 'goal_completion',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Time to complete
|
|
318
|
+
if (goal.completed_at) {
|
|
319
|
+
const durationDays = Math.ceil((goal.completed_at - goal.created_at) / 86400000);
|
|
320
|
+
createFact(entityId, 'days_to_complete', durationDays.toString(), {
|
|
321
|
+
confidence: 1.0,
|
|
322
|
+
source: 'goal_completion',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Estimation accuracy
|
|
327
|
+
if (goal.estimated_hours !== null && goal.actual_hours > 0) {
|
|
328
|
+
const accuracy = (goal.estimated_hours / goal.actual_hours).toFixed(2);
|
|
329
|
+
createFact(entityId, 'estimation_accuracy', accuracy, {
|
|
330
|
+
confidence: 1.0,
|
|
331
|
+
source: 'goal_completion',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Tags
|
|
336
|
+
if (goal.tags.length > 0) {
|
|
337
|
+
createFact(entityId, 'goal_tags', goal.tags.join(', '), {
|
|
338
|
+
confidence: 1.0,
|
|
339
|
+
source: 'goal_completion',
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.error('[Extractor] Failed to extract goal completion:', err);
|
|
344
|
+
}
|
|
345
|
+
}
|