@dxos/assistant 0.8.2 → 0.8.3-main.672df60

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.
@@ -2,41 +2,84 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { type ReadonlySignal, signal } from '@preact/signals-core';
5
6
  import chalk from 'chalk';
6
7
 
7
8
  import { ConsolePrinter } from '@dxos/ai';
9
+ import { type CleanupFn, combine } from '@dxos/async';
8
10
 
9
11
  import { type Blueprint } from './blueprint';
10
12
  import { type BlueprintMachine, type BlueprintMachineState } from './machine';
11
13
 
12
14
  /* eslint-disable no-console */
13
15
 
14
- // Force chalk colors on for tests
16
+ // Force chalk colors on for tests.
15
17
  chalk.level = 2;
16
18
 
17
- export const setConsolePrinter = (machine: BlueprintMachine, extraLogging = false) => {
19
+ export class Logger {
20
+ private _messages = signal<string[]>([]);
21
+
22
+ get messages(): ReadonlySignal<string[]> {
23
+ return this._messages;
24
+ }
25
+
26
+ clear() {
27
+ this._messages.value = [];
28
+ }
29
+
30
+ log(message: string) {
31
+ this._messages.value = [...this._messages.value, message];
32
+ }
33
+ }
34
+
35
+ export const setLogger = (machine: BlueprintMachine, logger: Logger): CleanupFn => {
36
+ return combine(
37
+ machine.begin.on(() => {
38
+ logger.log('Starting...');
39
+ }),
40
+ machine.stepStart.on((step) => {
41
+ const index = machine.blueprint.steps.indexOf(step);
42
+ logger.log(`Step ${index + 1} of ${machine.blueprint.steps.length}`);
43
+ logger.log(`Instructions: ${step.instructions}`);
44
+ }),
45
+ machine.stepComplete.on((step) => {
46
+ const trace = machine.state.trace.find((t) => t.stepId === step.id);
47
+ logger.log(`Complete: ${trace?.comment}`);
48
+ }),
49
+ machine.end.on(() => {
50
+ logger.log('OK');
51
+ }),
52
+ );
53
+ };
54
+
55
+ export const setConsolePrinter = (machine: BlueprintMachine, verbose = false): CleanupFn => {
18
56
  const printer = new ConsolePrinter();
19
- if (extraLogging) {
57
+ return combine(
58
+ machine.begin.on(() => printTrace(machine.blueprint, machine.state)),
20
59
  machine.stepStart.on((step) =>
21
60
  console.log(
22
61
  `\n${chalk.magenta(`${chalk.bold(`STEP ${machine.blueprint.steps.indexOf(step) + 1} of ${machine.blueprint.steps.length}:`)} ${step.instructions}`)}\n`,
23
62
  ),
24
- );
25
- machine.message.on((msg) => printer.printMessage(msg));
26
- machine.block.on((block) => printer.printContentBlock(block));
27
- }
28
- machine.begin.on(() => printTrace(machine.blueprint, machine.state));
29
- machine.stepComplete.on(() => printTrace(machine.blueprint, machine.state));
63
+ ),
64
+ machine.stepComplete.on(() => printTrace(machine.blueprint, machine.state)),
65
+ machine.end.on(() => console.log('DONE')),
66
+ verbose
67
+ ? [
68
+ machine.message.on((msg) => printer.printMessage(msg)),
69
+ machine.block.on((block) => printer.printContentBlock(block)),
70
+ ]
71
+ : [],
72
+ );
30
73
  };
31
74
 
32
- const printTrace = (blueprint: Blueprint, state: BlueprintMachineState) => {
33
- console.log('\n==============================================\n');
34
- console.log(chalk.bold('\nThe Blueprint:'));
75
+ const BREAK_LINE = `\n${'_'.repeat(80)}\n`;
35
76
 
36
- blueprint.steps.forEach((step, index) => {
77
+ const printTrace = (blueprint: Blueprint, state: BlueprintMachineState) => {
78
+ console.group(chalk.bold('\nBlueprint'));
79
+ blueprint.steps.forEach((step) => {
37
80
  const traceStep = state.trace.find((t) => t.stepId === step.id);
38
81
 
39
- let color = chalk.gray; // Not executed
82
+ let color = chalk.gray; // Not executed.
40
83
  let bullet = '○';
41
84
  if (traceStep) {
42
85
  switch (traceStep.status) {
@@ -56,11 +99,11 @@ const printTrace = (blueprint: Blueprint, state: BlueprintMachineState) => {
56
99
  }
57
100
 
58
101
  console.log(color(`\n${bullet} ${step.instructions}`));
59
-
60
102
  if (traceStep?.comment) {
61
- console.log(chalk.white(` ↳ ${traceStep.comment}`));
103
+ console.log(chalk.white(` ↳ ${traceStep.comment}`));
62
104
  }
63
105
  });
64
- console.log('\n');
65
- console.log('\n==============================================\n');
106
+
107
+ console.groupEnd();
108
+ console.log(BREAK_LINE);
66
109
  };
@@ -6,21 +6,28 @@ import { Match, Schema } from 'effect';
6
6
 
7
7
  import {
8
8
  createTool,
9
- type ExecutableTool,
10
- Message,
11
- ToolResult,
12
9
  type AIServiceClient,
10
+ Message,
13
11
  type MessageContentBlock,
12
+ ToolResult,
14
13
  type ToolUseContentBlock,
14
+ type ToolRegistry,
15
15
  } from '@dxos/ai';
16
16
  import { Event } from '@dxos/async';
17
17
  import { create } from '@dxos/echo-schema';
18
18
  import { type ObjectId } from '@dxos/keys';
19
19
  import { log } from '@dxos/log';
20
+ import { isNonNullable } from '@dxos/util';
20
21
 
21
22
  import type { Blueprint, BlueprintStep } from './blueprint';
22
23
  import { AISession } from '../session';
23
24
 
25
+ export type BlueprintTraceStep = {
26
+ status: 'done' | 'bailed' | 'skipped';
27
+ stepId: ObjectId;
28
+ comment: string;
29
+ };
30
+
24
31
  export type BlueprintMachineState = {
25
32
  history: Message[];
26
33
  trace: BlueprintTraceStep[];
@@ -33,12 +40,6 @@ const INITIAL_STATE: BlueprintMachineState = {
33
40
  trace: [],
34
41
  };
35
42
 
36
- export type BlueprintTraceStep = {
37
- status: 'done' | 'bailed' | 'skipped';
38
- stepId: ObjectId;
39
- comment: string;
40
- };
41
-
42
43
  type ExecutionOptions = {
43
44
  aiService: AIServiceClient;
44
45
 
@@ -61,7 +62,10 @@ export class BlueprintMachine {
61
62
 
62
63
  state: BlueprintMachineState = structuredClone(INITIAL_STATE);
63
64
 
64
- constructor(readonly blueprint: Blueprint) {}
65
+ constructor(
66
+ readonly registry: ToolRegistry,
67
+ readonly blueprint: Blueprint,
68
+ ) {}
65
69
 
66
70
  async runToCompletion(options: ExecutionOptions): Promise<void> {
67
71
  log.info('runToCompletion', options);
@@ -132,6 +136,8 @@ export class BlueprintMachine {
132
136
  : [];
133
137
  inputMessages.forEach((message) => this.message.emit(message));
134
138
 
139
+ // TODO(wittjosiah): Warn if tool is not found.
140
+ const tools = nextStep.tools.map((tool) => this.registry.get(tool)).filter(isNonNullable);
135
141
  const messages = await session.run({
136
142
  systemPrompt: `
137
143
  You are a smart Rule-Following Agent.
@@ -144,7 +150,7 @@ export class BlueprintMachine {
144
150
  The Rule-Following Agent precisely follows the instructions.
145
151
  `,
146
152
  history: [...state.history, ...inputMessages],
147
- tools: [...nextStep.tools, report] as ExecutableTool[], // TODO(burdon): REMOVE CAST!
153
+ tools: [...tools, report],
148
154
  artifacts: [],
149
155
  client: options.aiService,
150
156
  prompt: nextStep.instructions,
@@ -5,7 +5,8 @@
5
5
  import { Schema, identity, Option, SchemaAST } from 'effect';
6
6
 
7
7
  import { createTool, ToolResult } from '@dxos/ai';
8
- import { type EchoDatabase } from '@dxos/echo-db';
8
+ import type { Obj, Relation } from '@dxos/echo';
9
+ import { type EchoDatabase, type Queue } from '@dxos/echo-db';
9
10
  import { isEncodedReference } from '@dxos/echo-protocol';
10
11
  import {
11
12
  EntityKind,
@@ -20,14 +21,16 @@ import {
20
21
  ObjectId,
21
22
  Query,
22
23
  ReferenceAnnotationId,
24
+ type BaseObject,
25
+ RelationSourceDXNId,
26
+ RelationTargetDXNId,
23
27
  RelationSourceId,
24
28
  RelationTargetId,
25
- type BaseObject,
26
29
  } from '@dxos/echo-schema';
27
30
  import { mapAst } from '@dxos/effect';
28
31
  import { DXN } from '@dxos/keys';
29
32
  import { log } from '@dxos/log';
30
- import { deepMapValues } from '@dxos/util';
33
+ import { deepMapValues, isNonNullable } from '@dxos/util';
31
34
 
32
35
  // TODO(burdon): Unify with the graph schema.
33
36
  export const Subgraph = Schema.Struct({
@@ -95,7 +98,7 @@ const isSchemaAddressableByDxn = (schema: Schema.Schema.AnyNoContext, dxn: DXN):
95
98
  /**
96
99
  * Perform vector search in the local database.
97
100
  */
98
- export const createLocalSearchTool = (db: EchoDatabase) => {
101
+ export const createLocalSearchTool = (db: EchoDatabase, queue?: Queue) => {
99
102
  return createTool('search', {
100
103
  name: 'local_search',
101
104
  description: 'Search the local database for information using a vector index',
@@ -106,9 +109,16 @@ export const createLocalSearchTool = (db: EchoDatabase) => {
106
109
  }),
107
110
  execute: async ({ query }) => {
108
111
  const { objects } = await db.query(Query.select(Filter.text(query, { type: 'vector' }))).run();
112
+ const results = [...objects];
113
+ if (queue) {
114
+ const queueObjects = await queue.queryObjects();
115
+ // TODO(dmaretskyi): Text search on the queue.
116
+ results.push(...queueObjects);
117
+ }
118
+
109
119
  return ToolResult.Success(`
110
120
  <local_context>
111
- ${JSON.stringify(objects, null, 2)}
121
+ ${JSON.stringify(results, null, 2)}
112
122
  </local_context>
113
123
  `);
114
124
  },
@@ -141,6 +151,7 @@ export const sanitizeObjects = async (
141
151
  types: Schema.Schema.AnyNoContext[],
142
152
  data: Record<string, readonly unknown[]>,
143
153
  db: EchoDatabase,
154
+ queue?: Queue,
144
155
  ): Promise<BaseObject[]> => {
145
156
  const entries = types
146
157
  .map(
@@ -153,8 +164,8 @@ export const sanitizeObjects = async (
153
164
  .flat();
154
165
 
155
166
  const idMap = new Map<string, string>();
156
-
157
167
  const existingIds = new Set<ObjectId>();
168
+ const enitties = new Map<ObjectId, Obj.Any | Relation.Any>();
158
169
 
159
170
  const resolveId = (id: string): DXN | undefined => {
160
171
  if (ObjectId.isValid(id)) {
@@ -210,23 +221,58 @@ export const sanitizeObjects = async (
210
221
  }
211
222
  delete data.source;
212
223
  delete data.target;
213
- data[RelationSourceId] = sourceDxn;
214
- data[RelationTargetId] = targetDxn;
224
+ data[RelationSourceDXNId] = sourceDxn;
225
+ data[RelationTargetDXNId] = targetDxn;
215
226
  }
216
227
 
217
- return create(entry.schema, data);
228
+ return {
229
+ data,
230
+ schema: entry.schema,
231
+ };
218
232
  })
219
- .filter((object) => !existingIds.has(object.id)); // TODO(dmaretskyi): This dissallows updating existing objects.
233
+ .filter((object) => !existingIds.has(object.data.id)); // TODO(dmaretskyi): This dissallows updating existing objects.
234
+
235
+ // TODO(dmaretskyi): Use ref resolver.
236
+ const { objects: dbObjects } = await db.query(Query.select(Filter.ids(...existingIds))).run();
237
+ const queueObjects = (await queue?.getObjectsById([...existingIds])) ?? [];
238
+ const objects = [...dbObjects, ...queueObjects].filter(isNonNullable);
220
239
 
221
- const { objects } = await db.query(Query.select(Filter.ids(...existingIds))).run();
222
240
  // TODO(dmaretskyi): Returns everything if IDs are empty!
223
- log.info('objects', { objects, existingIds });
241
+ log.info('objects', { dbObjects, queueObjects, existingIds });
224
242
  const missing = Array.from(existingIds).filter((id) => !objects.some((object) => object.id === id));
225
243
  if (missing.length > 0) {
226
244
  throw new Error(`Object IDs do not point to existing objects: ${missing.join(', ')}`);
227
245
  }
228
246
 
229
- return res;
247
+ return res.flatMap(({ data, schema }) => {
248
+ let skip = false;
249
+ if (RelationSourceDXNId in data) {
250
+ const id = (data[RelationSourceDXNId] as DXN).asEchoDXN()?.echoId;
251
+ const obj = objects.find((object) => object.id === id) ?? enitties.get(id!);
252
+ if (obj) {
253
+ delete data[RelationSourceDXNId];
254
+ data[RelationSourceId] = obj;
255
+ } else {
256
+ skip = true;
257
+ }
258
+ }
259
+ if (RelationTargetDXNId in data) {
260
+ const id = (data[RelationTargetDXNId] as DXN).asEchoDXN()?.echoId;
261
+ const obj = objects.find((object) => object.id === id) ?? enitties.get(id!);
262
+ if (obj) {
263
+ delete data[RelationTargetDXNId];
264
+ data[RelationTargetId] = obj;
265
+ } else {
266
+ skip = true;
267
+ }
268
+ }
269
+ if (!skip) {
270
+ const obj = create(schema, data);
271
+ enitties.set(obj.id, obj);
272
+ return [obj];
273
+ }
274
+ return [];
275
+ });
230
276
  };
231
277
 
232
278
  const SoftRef = Schema.Struct({
@@ -272,10 +318,12 @@ const preprocessSchema = (schema: Schema.Schema.AnyNoContext) => {
272
318
 
273
319
  export const createGraphWriterTool = ({
274
320
  db,
321
+ queue,
275
322
  schemaTypes,
276
323
  onDone = async (x) => x,
277
324
  }: {
278
325
  db: EchoDatabase;
326
+ queue?: Queue;
279
327
  schemaTypes: Schema.Schema.AnyNoContext[];
280
328
  onDone?: (data: AnyEchoObject[]) => Promise<any>;
281
329
  }) => {
@@ -284,7 +332,7 @@ export const createGraphWriterTool = ({
284
332
  description: 'Write to the local graph database',
285
333
  schema: createExtractionSchema(schemaTypes),
286
334
  execute: async (input) => {
287
- const data = await sanitizeObjects(schemaTypes, input as any, db);
335
+ const data = await sanitizeObjects(schemaTypes, input as any, db, queue);
288
336
  return ToolResult.Success(await onDone(data as AnyEchoObject[]));
289
337
  },
290
338
  });