@dxos/assistant 0.8.2 → 0.8.3-main.7f5a14c

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.
@@ -6,21 +6,39 @@ 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
+ const SYSTEM_PROMPT = `
26
+ You are a smart Rule-Following Agent.
27
+ Rule-Following Agent executes the command that the user sent in the last message.
28
+ After doing the work, the Rule-Following Agent calls report tool.
29
+ If the Rule-Following agent believes that no action is needed, it calls the report tool with "skipped" status.
30
+ If the Rule-Following Agent is unable to perform the task, it calls the report tool with "bail" status.
31
+ Rule-Following Agent explains the reason it is unable to perform the task before bailing.
32
+ The Rule-Following Agent can express creativity and imagination in the way it performs the task.
33
+ The Rule-Following Agent precisely follows the instructions.
34
+ `;
35
+
36
+ export type BlueprintTraceStep = {
37
+ status: 'done' | 'bailed' | 'skipped';
38
+ stepId: ObjectId;
39
+ comment: string;
40
+ };
41
+
24
42
  export type BlueprintMachineState = {
25
43
  history: Message[];
26
44
  trace: BlueprintTraceStep[];
@@ -33,14 +51,8 @@ const INITIAL_STATE: BlueprintMachineState = {
33
51
  trace: [],
34
52
  };
35
53
 
36
- export type BlueprintTraceStep = {
37
- status: 'done' | 'bailed' | 'skipped';
38
- stepId: ObjectId;
39
- comment: string;
40
- };
41
-
42
54
  type ExecutionOptions = {
43
- aiService: AIServiceClient;
55
+ aiClient: AIServiceClient;
44
56
 
45
57
  /**
46
58
  * Input to the blueprint.
@@ -48,6 +60,18 @@ type ExecutionOptions = {
48
60
  input?: unknown;
49
61
  };
50
62
 
63
+ export type BlueprintEvent =
64
+ | { type: 'begin' }
65
+ | { type: 'end' }
66
+ | { type: 'step-start'; step: BlueprintStep }
67
+ | { type: 'step-complete'; step: BlueprintStep }
68
+ | { type: 'message'; message: Message }
69
+ | { type: 'block'; block: MessageContentBlock };
70
+
71
+ export interface BlueprintLogger {
72
+ log(event: BlueprintEvent): void;
73
+ }
74
+
51
75
  /**
52
76
  * Blueprint state machine.
53
77
  */
@@ -59,43 +83,62 @@ export class BlueprintMachine {
59
83
  public readonly message = new Event<Message>();
60
84
  public readonly block = new Event<MessageContentBlock>();
61
85
 
62
- state: BlueprintMachineState = structuredClone(INITIAL_STATE);
86
+ // TODO(burdon): Replace events with logger?
87
+ private logger?: BlueprintLogger;
63
88
 
64
- constructor(readonly blueprint: Blueprint) {}
89
+ private _state: BlueprintMachineState = structuredClone(INITIAL_STATE);
90
+
91
+ constructor(
92
+ readonly registry: ToolRegistry,
93
+ readonly blueprint: Blueprint,
94
+ ) {}
95
+
96
+ get state(): BlueprintMachineState {
97
+ return this._state;
98
+ }
99
+
100
+ setLogger(logger: BlueprintLogger): this {
101
+ this.logger = logger;
102
+ return this;
103
+ }
65
104
 
66
105
  async runToCompletion(options: ExecutionOptions): Promise<void> {
67
106
  log.info('runToCompletion', options);
68
107
 
69
108
  this.begin.emit();
109
+ this.logger?.log({ type: 'begin' });
110
+
70
111
  let firstStep = true;
71
- while (this.state.state !== 'done') {
112
+ while (this._state.state !== 'done') {
72
113
  const input = firstStep ? options.input : undefined;
73
-
74
114
  firstStep = false;
75
- this.state = await this._execStep(this.state, {
76
- input,
77
- aiService: options.aiService,
78
- });
79
115
 
80
- this.stepComplete.emit(this.blueprint.steps.find((step) => step.id === this.state.trace.at(-1)?.stepId)!);
116
+ this._state = await this._execStep(this._state, { input, aiClient: options.aiClient });
117
+
118
+ const step = this.blueprint.steps.find((step) => step.id === this._state.trace.at(-1)?.stepId)!;
119
+ this.stepComplete.emit(step);
120
+ this.logger?.log({ type: 'step-complete', step });
81
121
 
82
- if (this.state.state === 'bail') {
122
+ if (this._state.state === 'bail') {
83
123
  throw new Error('Agent unable to follow the blueprint');
84
124
  }
85
125
  }
86
126
 
87
127
  this.end.emit();
128
+ this.logger?.log({ type: 'end' });
88
129
  }
89
130
 
90
131
  private async _execStep(state: BlueprintMachineState, options: ExecutionOptions): Promise<BlueprintMachineState> {
91
- const prevStep = this.blueprint.steps.findIndex((step) => step.id === this.state.trace.at(-1)?.stepId);
132
+ const prevStep = this.blueprint.steps.findIndex((step) => step.id === this._state.trace.at(-1)?.stepId);
92
133
  if (prevStep === this.blueprint.steps.length - 1) {
93
134
  throw new Error('Done execution blueprint');
94
135
  }
95
136
 
96
137
  const nextStep = this.blueprint.steps[prevStep + 1];
97
138
  const onLastStep = prevStep === this.blueprint.steps.length - 2;
139
+
98
140
  this.stepStart.emit(nextStep);
141
+ this.logger?.log({ type: 'step-start', step: nextStep });
99
142
 
100
143
  const ReportSchema = Schema.Struct({
101
144
  status: Schema.Literal('done', 'bailed', 'skipped').annotations({
@@ -109,7 +152,7 @@ export class BlueprintMachine {
109
152
 
110
153
  const report = createTool('system', {
111
154
  name: 'report',
112
- description: 'This tool reports that the agent has completed the task or is unable to do so',
155
+ description: 'This tool reports that the agent has completed the task or is unable to do so.',
113
156
  schema: ReportSchema,
114
157
  execute: async (input) => ToolResult.Break(input),
115
158
  });
@@ -130,30 +173,25 @@ export class BlueprintMachine {
130
173
  }),
131
174
  ]
132
175
  : [];
133
- inputMessages.forEach((message) => this.message.emit(message));
176
+ inputMessages.forEach((message) => {
177
+ this.message.emit(message);
178
+ this.logger?.log({ type: 'message', message });
179
+ });
134
180
 
181
+ // TODO(wittjosiah): Warn if tool is not found.
182
+ const tools = nextStep.tools?.map((tool) => this.registry.get(tool)).filter(isNonNullable) ?? [];
135
183
  const messages = await session.run({
136
- systemPrompt: `
137
- You are a smart Rule-Following Agent.
138
- Rule-Following Agent executes the command that the user sent in the last message.
139
- After doing the work, the Rule-Following Agent calls report tool.
140
- If the Rule-Following agent believes that no action is needed, it calls the report tool with "skipped" status.
141
- If the Rule-Following Agent is unable to perform the task, it calls the report tool with "bail" status.
142
- Rule-Following Agent explains the reason it is unable to perform the task before bailing.
143
- The Rule-Following Agent can express creativity and imagination in the way it performs the task.
144
- The Rule-Following Agent precisely follows the instructions.
145
- `,
184
+ systemPrompt: SYSTEM_PROMPT,
146
185
  history: [...state.history, ...inputMessages],
147
- tools: [...nextStep.tools, report] as ExecutableTool[], // TODO(burdon): REMOVE CAST!
186
+ tools: [...tools, report],
148
187
  artifacts: [],
149
- client: options.aiService,
188
+ client: options.aiClient,
150
189
  prompt: nextStep.instructions,
151
190
  });
152
191
 
153
192
  const { messages: trimmedHistory, call: lastBlock } = popLastToolCall(messages);
154
-
155
193
  if (!lastBlock) {
156
- // TODO(dmaretskyi): Handle this with grace
194
+ // TODO(dmaretskyi): Handle this with grace.
157
195
  throw new Error('Agent did not call the report tool');
158
196
  }
159
197
 
@@ -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,19 +318,21 @@ const preprocessSchema = (schema: Schema.Schema.AnyNoContext) => {
272
318
 
273
319
  export const createGraphWriterTool = ({
274
320
  db,
275
- schemaTypes,
321
+ queue,
322
+ schema,
276
323
  onDone = async (x) => x,
277
324
  }: {
278
325
  db: EchoDatabase;
279
- schemaTypes: Schema.Schema.AnyNoContext[];
326
+ queue?: Queue;
327
+ schema: Schema.Schema.AnyNoContext[];
280
328
  onDone?: (data: AnyEchoObject[]) => Promise<any>;
281
329
  }) => {
282
330
  return createTool('graph', {
283
331
  name: 'writer',
284
332
  description: 'Write to the local graph database',
285
- schema: createExtractionSchema(schemaTypes),
333
+ schema: createExtractionSchema(schema),
286
334
  execute: async (input) => {
287
- const data = await sanitizeObjects(schemaTypes, input as any, db);
335
+ const data = await sanitizeObjects(schema, input as any, db, queue);
288
336
  return ToolResult.Success(await onDone(data as AnyEchoObject[]));
289
337
  },
290
338
  });
@@ -20,7 +20,7 @@ import { researchFn } from './research';
20
20
  const REMOTE_AI = true;
21
21
  const MOCK_SEARCH = false;
22
22
 
23
- describe('Research', () => {
23
+ describe.skip('Research', () => {
24
24
  let builder: EchoTestBuilder;
25
25
  let db: EchoDatabase;
26
26
  let executor: FunctionExecutor;
@@ -60,7 +60,7 @@ export const researchFn = defineFunction({
60
60
  });
61
61
  session.streamEvent.on((event) => log('stream', { event }));
62
62
 
63
- const graphWriteTool = createGraphWriterTool({ db, schemaTypes: DataTypes });
63
+ const graphWriteTool = createGraphWriterTool({ db, schema: DataTypes });
64
64
  log.info('graphWriteTool', { schema: graphWriteTool.parameters });
65
65
 
66
66
  // TODO(dmaretskyi): Consider adding this pattern as the "Graph" output mode for the session.