@dxos/assistant 0.8.3 → 0.8.4-main.3a94e84

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.
Files changed (103) hide show
  1. package/dist/lib/browser/index.mjs +760 -1087
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +760 -1087
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/context/binder.d.ts +34 -0
  8. package/dist/types/src/context/binder.d.ts.map +1 -0
  9. package/dist/types/src/context/binding.d.ts +56 -0
  10. package/dist/types/src/context/binding.d.ts.map +1 -0
  11. package/dist/types/src/context/index.d.ts +3 -0
  12. package/dist/types/src/context/index.d.ts.map +1 -0
  13. package/dist/types/src/conversation/conversation.d.ts +38 -0
  14. package/dist/types/src/conversation/conversation.d.ts.map +1 -0
  15. package/dist/types/src/conversation/index.d.ts +2 -0
  16. package/dist/types/src/conversation/index.d.ts.map +1 -0
  17. package/dist/types/src/errors.d.ts +38 -0
  18. package/dist/types/src/errors.d.ts.map +1 -0
  19. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +1 -1
  20. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +135 -41
  21. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +1 -1
  22. package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts +1 -1
  23. package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts.map +1 -1
  24. package/dist/types/src/extraction/extraction.d.ts +100 -42
  25. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  26. package/dist/types/src/functions/index.d.ts +2 -0
  27. package/dist/types/src/functions/index.d.ts.map +1 -0
  28. package/dist/types/src/functions/services.d.ts +6 -0
  29. package/dist/types/src/functions/services.d.ts.map +1 -0
  30. package/dist/types/src/index.d.ts +4 -1
  31. package/dist/types/src/index.d.ts.map +1 -1
  32. package/dist/types/src/research/exa.d.ts +10 -5
  33. package/dist/types/src/research/exa.d.ts.map +1 -1
  34. package/dist/types/src/research/graph.d.ts +18 -8
  35. package/dist/types/src/research/graph.d.ts.map +1 -1
  36. package/dist/types/src/research/research.d.ts +1 -1
  37. package/dist/types/src/research/research.d.ts.map +1 -1
  38. package/dist/types/src/session/session.d.ts +183 -184
  39. package/dist/types/src/session/session.d.ts.map +1 -1
  40. package/dist/types/src/session/version-pin.d.ts +2 -2
  41. package/dist/types/src/session/version-pin.d.ts.map +1 -1
  42. package/dist/types/src/templates/index.d.ts +2 -0
  43. package/dist/types/src/templates/index.d.ts.map +1 -0
  44. package/dist/types/src/templates/prompt.d.ts +30 -0
  45. package/dist/types/src/templates/prompt.d.ts.map +1 -0
  46. package/dist/types/src/types.d.ts +10 -0
  47. package/dist/types/src/types.d.ts.map +1 -1
  48. package/dist/types/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +29 -30
  50. package/src/context/binder.ts +103 -0
  51. package/src/context/binding.ts +32 -0
  52. package/src/context/index.ts +6 -0
  53. package/src/conversation/conversation.ts +96 -0
  54. package/src/conversation/index.ts +5 -0
  55. package/src/errors.ts +10 -0
  56. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +6 -18
  57. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +19 -14
  58. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +6 -3
  59. package/src/extraction/extraction-ner-function/named-entity-recognition.test.ts +2 -2
  60. package/src/extraction/extraction-ner-function/named-entity-recognition.ts +1 -1
  61. package/src/functions/index.ts +5 -0
  62. package/src/functions/services.ts +139 -0
  63. package/src/index.ts +4 -1
  64. package/src/research/exa.ts +56 -59
  65. package/src/research/graph.test.ts +50 -1
  66. package/src/research/graph.ts +85 -53
  67. package/src/research/instructions-research.tpl +1 -0
  68. package/src/research/research.test.ts +27 -27
  69. package/src/research/research.ts +50 -52
  70. package/src/session/__snapshots__/session.test.ts.snap +32 -0
  71. package/src/session/session.test.ts +99 -18
  72. package/src/session/session.ts +230 -443
  73. package/src/session/version-pin.test.ts +2 -21
  74. package/src/session/version-pin.ts +5 -5
  75. package/src/summarization/summarization.test.ts +75 -88
  76. package/src/templates/assistant.tpl +111 -0
  77. package/src/templates/index.ts +5 -0
  78. package/src/templates/prompt.ts +45 -0
  79. package/src/types.ts +40 -1
  80. package/dist/lib/node/index.cjs +0 -2178
  81. package/dist/lib/node/index.cjs.map +0 -7
  82. package/dist/lib/node/meta.json +0 -1
  83. package/dist/types/src/blueprint/ai.test.d.ts +0 -2
  84. package/dist/types/src/blueprint/ai.test.d.ts.map +0 -1
  85. package/dist/types/src/blueprint/blueprint.d.ts +0 -55
  86. package/dist/types/src/blueprint/blueprint.d.ts.map +0 -1
  87. package/dist/types/src/blueprint/blueprint.test.d.ts +0 -2
  88. package/dist/types/src/blueprint/blueprint.test.d.ts.map +0 -1
  89. package/dist/types/src/blueprint/index.d.ts +0 -4
  90. package/dist/types/src/blueprint/index.d.ts.map +0 -1
  91. package/dist/types/src/blueprint/logger.d.ts +0 -25
  92. package/dist/types/src/blueprint/logger.d.ts.map +0 -1
  93. package/dist/types/src/blueprint/machine.d.ts +0 -143
  94. package/dist/types/src/blueprint/machine.d.ts.map +0 -1
  95. package/dist/types/src/blueprint/test-data.d.ts +0 -3
  96. package/dist/types/src/blueprint/test-data.d.ts.map +0 -1
  97. package/src/blueprint/ai.test.ts +0 -156
  98. package/src/blueprint/blueprint.test.ts +0 -151
  99. package/src/blueprint/blueprint.ts +0 -75
  100. package/src/blueprint/index.ts +0 -7
  101. package/src/blueprint/logger.ts +0 -155
  102. package/src/blueprint/machine.ts +0 -251
  103. package/src/blueprint/test-data.ts +0 -55
@@ -2,78 +2,75 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import { AiTool, AiToolkit } from '@effect/ai';
6
+ import { Effect, Schema } from 'effect';
6
7
  import Exa from 'exa-js';
7
8
 
8
- import { createTool, ToolResult } from '@dxos/ai';
9
+ import { CredentialsService } from '@dxos/functions';
9
10
  import { log } from '@dxos/log';
10
11
 
11
12
  import { SEARCH_RESULTS } from '../testing';
12
13
 
13
- type CreateExaToolOptions = {
14
- apiKey: string;
15
- };
16
-
17
- export const createExaTool = ({ apiKey }: CreateExaToolOptions) => {
18
- const exa = new Exa(apiKey);
19
-
20
- return createTool('search', {
21
- name: 'web_search',
14
+ export class ExaToolkit extends AiToolkit.make(
15
+ AiTool.make('exa_search', {
22
16
  description: 'Search the web for information',
23
- schema: Schema.Struct({
17
+ parameters: {
24
18
  query: Schema.String.annotations({ description: 'The query to search for' }),
25
- }),
26
- execute: async ({ query }) => {
27
- const context = await exa.searchAndContents(query, {
19
+ },
20
+ success: Schema.Unknown,
21
+ failure: Schema.Never,
22
+ }).addRequirement<CredentialsService>(),
23
+ ) {}
24
+
25
+ export const LiveExaHandler = ExaToolkit.toLayer({
26
+ exa_search: Effect.fn(function* ({ query }) {
27
+ const credential = yield* CredentialsService.getCredential({ service: 'exa.ai' });
28
+ const exa = new Exa(credential.apiKey);
29
+
30
+ const context = yield* Effect.promise(async () =>
31
+ exa.searchAndContents(query, {
28
32
  type: 'auto',
29
33
  text: {
30
34
  maxCharacters: 3_000,
31
35
  },
32
36
  livecrawl: 'always',
33
- });
34
-
35
- log.info('exa search', { query, costDollars: context.costDollars });
36
-
37
- return ToolResult.Success(context);
38
- },
39
- });
40
- };
41
-
42
- export const createMockExaTool = () => {
43
- return createTool('search', {
44
- name: 'web_search',
45
- description: 'Search the web for information',
46
- schema: Schema.Struct({
47
- query: Schema.String.annotations({ description: 'The query to search for' }),
48
- }),
49
- execute: async ({ query }) => {
50
- // Find result with closest matching autoprompt using weighted Levenshtein distance
51
- const result = SEARCH_RESULTS.reduce(
52
- (closest, current) => {
53
- if (!current.autopromptString) {
54
- return closest;
55
- }
56
- if (!closest) {
57
- return current;
58
- }
59
-
60
- // Calculate Levenshtein distance
61
- const dist1 = levenshteinDistance(query, current.autopromptString);
62
- const dist2 = levenshteinDistance(query, closest.autopromptString || '');
63
-
64
- // Weight by length of the longer string to normalize
65
- const weight1 = dist1 / Math.max(query.length, current.autopromptString.length);
66
- const weight2 = dist2 / Math.max(query.length, closest.autopromptString?.length || 0);
67
-
68
- return weight1 < weight2 ? current : closest;
69
- },
70
- null as (typeof SEARCH_RESULTS)[0] | null,
71
- );
72
-
73
- return ToolResult.Success(result);
74
- },
75
- });
76
- };
37
+ }),
38
+ );
39
+
40
+ log.info('exa search', { query, costDollars: context.costDollars });
41
+
42
+ return context;
43
+ }),
44
+ });
45
+
46
+ export const MockExaHandler = ExaToolkit.toLayer({
47
+ exa_search: Effect.fn(function* ({ query }) {
48
+ // Find result with closest matching autoprompt using weighted Levenshtein distance
49
+ const result = SEARCH_RESULTS.reduce(
50
+ (closest, current) => {
51
+ if (!current.autopromptString) {
52
+ return closest;
53
+ }
54
+ if (!closest) {
55
+ return current;
56
+ }
57
+
58
+ // Calculate Levenshtein distance
59
+ const dist1 = levenshteinDistance(query, current.autopromptString);
60
+ const dist2 = levenshteinDistance(query, closest.autopromptString || '');
61
+
62
+ // Weight by length of the longer string to normalize
63
+ const weight1 = dist1 / Math.max(query.length, current.autopromptString.length);
64
+ const weight2 = dist2 / Math.max(query.length, closest.autopromptString?.length || 0);
65
+
66
+ return weight1 < weight2 ? current : closest;
67
+ },
68
+ null as (typeof SEARCH_RESULTS)[0] | null,
69
+ );
70
+
71
+ return result;
72
+ }),
73
+ });
77
74
 
78
75
  const levenshteinDistance = (str1: string, str2: string): number => {
79
76
  const m = str1.length;
@@ -2,14 +2,63 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { describe } from 'vitest';
5
+ import { AnthropicClient } from '@effect/ai-anthropic';
6
+ import { FetchHttpClient } from '@effect/platform';
7
+ import { describe, it } from '@effect/vitest';
8
+ import { Config, Effect, Layer, pipe } from 'effect';
9
+
10
+ import { AiService, AiServiceRouter, ToolExecutionService, ToolResolverService } from '@dxos/ai';
11
+ import { tapHttpErrors } from '@dxos/ai/testing';
12
+ import { TestHelpers } from '@dxos/effect';
13
+ import { DatabaseService } from '@dxos/functions';
14
+ import { log } from '@dxos/log';
15
+ import { DataType } from '@dxos/schema';
16
+
17
+ import { makeGraphWriterHandler, makeGraphWriterToolkit } from './graph';
18
+ import { AiSession } from '../session';
6
19
 
7
20
  // import { type EchoTestBuilder } from '@dxos/echo-db/testing';
8
21
 
22
+ const TestLayer = pipe(
23
+ AiService.model('@anthropic/claude-3-5-sonnet-20241022'),
24
+ Layer.provideMerge(DatabaseService.notAvailable),
25
+ Layer.provideMerge(ToolResolverService.layerEmpty),
26
+ Layer.provideMerge(ToolExecutionService.layerEmpty),
27
+ Layer.provide(AiServiceRouter.AiServiceRouter),
28
+ Layer.provide(
29
+ AnthropicClient.layerConfig({
30
+ apiKey: Config.redacted('ANTHROPIC_API_KEY'),
31
+ transformClient: tapHttpErrors,
32
+ }),
33
+ ),
34
+ Layer.provide(FetchHttpClient.layer),
35
+ );
36
+
9
37
  describe('graph', () => {
10
38
  // let builder: EchoTestBuilder;
11
39
  // test('findRelatedSchema', async () => {
12
40
  // const db = await createEchoDatabase();
13
41
  // const relatedSchemas = await findRelatedSchema(db, Schema.Struct({}));
14
42
  // });
43
+
44
+ it.effect.skip(
45
+ 'calculator',
46
+ Effect.fn(
47
+ function* ({ expect }) {
48
+ const graphWriteToolkit = makeGraphWriterToolkit({ schema: [DataType.Project] });
49
+
50
+ const session = new AiSession();
51
+ const response = yield* session
52
+ .run({
53
+ prompt: 'What is 10 + 20?',
54
+ history: [],
55
+ toolkit: graphWriteToolkit,
56
+ })
57
+ .pipe(Effect.provide(makeGraphWriterHandler(graphWriteToolkit)));
58
+ log.info('response', { response });
59
+ },
60
+ Effect.provide(TestLayer),
61
+ TestHelpers.runIf(process.env.ANTHROPIC_API_KEY),
62
+ ),
63
+ );
15
64
  });
@@ -2,30 +2,31 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema, identity, Option, SchemaAST } from 'effect';
5
+ import { AiTool, AiToolkit } from '@effect/ai';
6
+ import { Context, Effect, identity, Option, Schema, SchemaAST } from 'effect';
6
7
 
7
- import { createTool, ToolResult } from '@dxos/ai';
8
8
  import type { Obj, Relation } from '@dxos/echo';
9
- import { Query, Filter } from '@dxos/echo';
9
+ import { Filter, Query } from '@dxos/echo';
10
10
  import { type EchoDatabase, type Queue } from '@dxos/echo-db';
11
11
  import { isEncodedReference } from '@dxos/echo-protocol';
12
12
  import {
13
+ create,
13
14
  EntityKind,
15
+ getEntityKind,
16
+ getSchemaDXN,
14
17
  getSchemaTypename,
15
18
  getTypeAnnotation,
16
19
  getTypeIdentifierAnnotation,
17
- create,
18
- getEntityKind,
19
- getSchemaDXN,
20
20
  ObjectId,
21
21
  ReferenceAnnotationId,
22
- type BaseObject,
23
22
  RelationSourceDXNId,
24
- RelationTargetDXNId,
25
23
  RelationSourceId,
24
+ RelationTargetDXNId,
26
25
  RelationTargetId,
26
+ type BaseObject,
27
27
  } from '@dxos/echo-schema';
28
28
  import { mapAst } from '@dxos/effect';
29
+ import { ContextQueueService, DatabaseService } from '@dxos/functions';
29
30
  import { DXN } from '@dxos/keys';
30
31
  import { log } from '@dxos/log';
31
32
  import { deepMapValues, isNonNullable } from '@dxos/util';
@@ -96,33 +97,80 @@ const isSchemaAddressableByDxn = (schema: Schema.Schema.AnyNoContext, dxn: DXN):
96
97
  /**
97
98
  * Perform vector search in the local database.
98
99
  */
99
- export const createLocalSearchTool = (db: EchoDatabase, queue?: Queue) => {
100
- return createTool('search', {
101
- name: 'local_search',
100
+ // TODO(dmaretskyi): Rename `GraphReadToolkit`.
101
+ export class LocalSearchToolkit extends AiToolkit.make(
102
+ AiTool.make('search_local_search', {
102
103
  description: 'Search the local database for information using a vector index',
103
- schema: Schema.Struct({
104
+ parameters: {
104
105
  query: Schema.String.annotations({
105
106
  description: 'The query to search for. Could be a question or a topic or a set of keywords.',
106
107
  }),
107
- }),
108
- execute: async ({ query }) => {
109
- const { objects } = await db.query(Query.select(Filter.text(query, { type: 'vector' }))).run();
110
- const results = [...objects];
111
- if (queue) {
112
- const queueObjects = await queue.queryObjects();
113
- // TODO(dmaretskyi): Text search on the queue.
114
- results.push(...queueObjects);
115
- }
116
-
117
- return ToolResult.Success(`
118
- <local_context>
119
- ${JSON.stringify(results, null, 2)}
120
- </local_context>
121
- `);
122
108
  },
123
- });
109
+ success: Schema.Unknown,
110
+ failure: Schema.Never,
111
+ }).addRequirement<DatabaseService>(),
112
+ ) {}
113
+
114
+ export const LocalSearchHandler = LocalSearchToolkit.toLayer({
115
+ search_local_search: Effect.fn(function* ({ query }) {
116
+ const { objects } = yield* DatabaseService.runQuery(Query.select(Filter.text(query, { type: 'vector' })));
117
+ const results = [...objects];
118
+
119
+ const contextQueue = yield* Effect.serviceOption(ContextQueueService);
120
+ if (Option.isSome(contextQueue)) {
121
+ const queueObjects = yield* Effect.promise(() => contextQueue.value.contextQueue.queryObjects());
122
+ // TODO(dmaretskyi): Text search on the queue.
123
+ results.push(...queueObjects);
124
+ }
125
+
126
+ return `
127
+ <local_context>
128
+ ${JSON.stringify(results, null, 2)}
129
+ </local_context>
130
+ `;
131
+ }),
132
+ });
133
+
134
+ /**
135
+ * Attached as an annotation to the writer tool.
136
+ */
137
+ class GraphWriterSchema extends Context.Tag('GraphWriterSchema')<
138
+ GraphWriterSchema,
139
+ { schema: Schema.Schema.AnyNoContext[] }
140
+ >() {}
141
+
142
+ /**
143
+ * Forms typed objects that can be written to the graph database.
144
+ */
145
+ export const makeGraphWriterToolkit = ({ schema }: { schema: Schema.Schema.AnyNoContext[] }) => {
146
+ return AiToolkit.make(
147
+ AiTool.make('graph_writer', {
148
+ description: 'Write to the local graph database',
149
+ parameters: createExtractionSchema(schema).fields,
150
+ success: Schema.Unknown,
151
+ failure: Schema.Never,
152
+ })
153
+ .addRequirement<DatabaseService>()
154
+ .annotateContext(Context.make(GraphWriterSchema, { schema })),
155
+ );
124
156
  };
125
157
 
158
+ export const makeGraphWriterHandler = (toolkit: ReturnType<typeof makeGraphWriterToolkit>) => {
159
+ const { schema } = Context.get(
160
+ toolkit.tools.graph_writer.annotations as Context.Context<GraphWriterSchema>,
161
+ GraphWriterSchema,
162
+ );
163
+ return toolkit.toLayer({
164
+ graph_writer: Effect.fn(function* (input) {
165
+ const { db } = yield* DatabaseService;
166
+ const contextQueue = yield* Effect.serviceOption(ContextQueueService);
167
+ const data = yield* Effect.promise(() =>
168
+ sanitizeObjects(schema, input as any, db, contextQueue.pipe(Option.getOrUndefined)?.contextQueue),
169
+ );
170
+ return data as Obj.Any[];
171
+ }),
172
+ });
173
+ };
126
174
  /**
127
175
  * Create a schema for structured data extraction.
128
176
  */
@@ -282,15 +330,21 @@ const SoftRef = Schema.Struct({
282
330
  const preprocessSchema = (schema: Schema.Schema.AnyNoContext) => {
283
331
  const isRelationSchema = getEntityKind(schema) === 'relation';
284
332
 
285
- const go = (ast: SchemaAST.AST): SchemaAST.AST => {
333
+ const go = (ast: SchemaAST.AST, visited = new Set<SchemaAST.AST>()): SchemaAST.AST => {
334
+ if (visited.has(ast)) {
335
+ // Already visited this node, prevent infinite recursion.
336
+ return ast;
337
+ }
338
+ visited.add(ast);
339
+
286
340
  if (SchemaAST.getAnnotation(ast, ReferenceAnnotationId).pipe(Option.isSome)) {
287
341
  return SoftRef.ast;
288
342
  }
289
343
 
290
- return mapAst(ast, go);
344
+ return mapAst(ast, (child) => go(child, visited));
291
345
  };
292
346
 
293
- return Schema.make<any, any, never>(mapAst(schema.ast, go)).pipe(
347
+ return Schema.make<any, any, never>(mapAst(schema.ast, (ast) => go(ast))).pipe(
294
348
  Schema.omit('id'),
295
349
  Schema.extend(
296
350
  Schema.Struct({
@@ -313,25 +367,3 @@ const preprocessSchema = (schema: Schema.Schema.AnyNoContext) => {
313
367
  : identity<Schema.Schema.AnyNoContext>,
314
368
  );
315
369
  };
316
-
317
- export const createGraphWriterTool = ({
318
- db,
319
- queue,
320
- schema,
321
- onDone = async (x) => x,
322
- }: {
323
- db: EchoDatabase;
324
- queue?: Queue;
325
- schema: Schema.Schema.AnyNoContext[];
326
- onDone?: (data: Obj.Any[]) => Promise<any>;
327
- }) => {
328
- return createTool('graph', {
329
- name: 'writer',
330
- description: 'Write to the local graph database',
331
- schema: createExtractionSchema(schema),
332
- execute: async (input) => {
333
- const data = await sanitizeObjects(schema, input as any, db, queue);
334
- return ToolResult.Success(await onDone(data as Obj.Any[]));
335
- },
336
- });
337
- };
@@ -25,6 +25,7 @@ The Research Agent always follows these principles:
25
25
  - The Research Agent creates relations and references between new objects found and what's already in the database.
26
26
  - The Research Agent does not create objects that are already in the database.
27
27
  - The Research Agent re-uses existing object IDs as references when enriching existing objects.
28
+ - The Research Agent ALWAYS calls the `graph_writer` at the end to save the data. This conversation will be delted, so only the data written to the graph will be preserved.
28
29
 
29
30
  The Research Agent may be asked for:
30
31
 
@@ -2,11 +2,14 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { AnthropicClient } from '@effect/ai-anthropic';
6
+ import { FetchHttpClient } from '@effect/platform';
7
+ import { Config, Layer, ManagedRuntime } from 'effect';
5
8
  import { inspect } from 'node:util';
6
- import { beforeAll, describe, test } from 'vitest';
9
+ import { afterAll, beforeAll, describe, test } from 'vitest';
7
10
 
8
- import { EdgeAiServiceClient, OllamaAiServiceClient, structuredOutputParser } from '@dxos/ai';
9
- import { AI_SERVICE_ENDPOINT, EXA_API_KEY } from '@dxos/ai/testing';
11
+ import { AiService, AiServiceRouter, structuredOutputParser } from '@dxos/ai';
12
+ import { EXA_API_KEY, tapHttpErrors } from '@dxos/ai/testing';
10
13
  import { Obj } from '@dxos/echo';
11
14
  import { type EchoDatabase } from '@dxos/echo-db';
12
15
  import { EchoTestBuilder } from '@dxos/echo-db/testing';
@@ -17,38 +20,35 @@ import { DataType, DataTypes } from '@dxos/schema';
17
20
  import { createExtractionSchema, getSanitizedSchemaName } from './graph';
18
21
  import { researchFn } from './research';
19
22
 
20
- const REMOTE_AI = true;
21
23
  const MOCK_SEARCH = false;
22
24
 
25
+ const AnthropicLayer = AnthropicClient.layerConfig({
26
+ apiKey: Config.redacted('ANTHROPIC_API_KEY'),
27
+ transformClient: tapHttpErrors,
28
+ });
29
+
30
+ const AiServiceLayer = AiServiceRouter.AiServiceRouter.pipe(
31
+ Layer.provide(AnthropicLayer),
32
+ Layer.provide(FetchHttpClient.layer),
33
+ );
34
+
23
35
  describe.skip('Research', () => {
36
+ let runtime: ManagedRuntime.ManagedRuntime<AiService, any>;
24
37
  let builder: EchoTestBuilder;
25
38
  let db: EchoDatabase;
26
39
  let executor: FunctionExecutor;
27
40
 
28
41
  beforeAll(async () => {
42
+ runtime = ManagedRuntime.make(AiServiceLayer);
43
+
29
44
  // TODO(dmaretskyi): Helper to scaffold this from a config.
30
45
  builder = await new EchoTestBuilder().open();
31
46
 
32
47
  db = (await builder.createDatabase({ indexing: { vector: true } })).db;
33
- db.graph.schemaRegistry.addSchema(DataTypes);
34
48
 
35
49
  executor = new FunctionExecutor(
36
50
  new ServiceContainer().setServices({
37
- ai: {
38
- client: REMOTE_AI
39
- ? new EdgeAiServiceClient({
40
- endpoint: AI_SERVICE_ENDPOINT.REMOTE,
41
- defaultGenerationOptions: {
42
- // model: '@anthropic/claude-sonnet-4-20250514',
43
- model: '@anthropic/claude-3-5-sonnet-20241022',
44
- },
45
- })
46
- : new OllamaAiServiceClient({
47
- overrides: {
48
- model: 'llama3.1:8b',
49
- },
50
- }),
51
- },
51
+ ai: await runtime.runPromise(AiService),
52
52
  credentials: new ConfiguredCredentialsService([{ service: 'exa.ai', apiKey: EXA_API_KEY }]),
53
53
  database: { db },
54
54
  tracing: TracingService.console,
@@ -56,13 +56,13 @@ describe.skip('Research', () => {
56
56
  );
57
57
  });
58
58
 
59
- test('should generate a research report', { timeout: 300_000 }, async () => {
60
- db.add(
61
- Obj.make(DataType.Organization, {
62
- name: 'Notion',
63
- website: 'https://www.notion.com',
64
- }),
65
- );
59
+ afterAll(async () => {
60
+ await runtime.dispose();
61
+ });
62
+
63
+ // TODO(dmaretskyi): Refactor using effect.
64
+ test.only('should generate a research report', { timeout: 300_000 }, async () => {
65
+ db.add(Obj.make(DataType.Organization, { name: 'Notion', website: 'https://www.notion.com' }));
66
66
  await db.flush({ indexes: true });
67
67
 
68
68
  const result = await executor.invoke(researchFn, {
@@ -2,19 +2,22 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ // TODO(burdon): Fix.
6
+ // @ts-nocheck
6
7
 
7
- import { AgentStatusReport, ConsolePrinter } from '@dxos/ai';
8
- import { create } from '@dxos/echo-schema';
9
- import { AiService, CredentialsService, DatabaseService, defineFunction, TracingService } from '@dxos/functions';
8
+ import { AiToolkit } from '@effect/ai';
9
+ import { Effect, Layer, Schema } from 'effect';
10
+
11
+ import { AiService, ConsolePrinter } from '@dxos/ai';
12
+ import { defineFunction, TracingService } from '@dxos/functions';
10
13
  import { log } from '@dxos/log';
11
14
  import { DataTypes } from '@dxos/schema';
12
15
 
13
- import { createExaTool, createMockExaTool } from './exa';
14
- import { createGraphWriterTool } from './graph';
16
+ import { ExaToolkit, LiveExaHandler, MockExaHandler } from './exa';
17
+ import { LocalSearchHandler, LocalSearchToolkit, makeGraphWriterHandler, makeGraphWriterToolkit } from './graph';
15
18
  // TODO(dmaretskyi): Vite build bug with instruction files with the same filename getting mixed-up
16
19
  import PROMPT from './instructions-research.tpl?raw';
17
- import { AISession } from '../session';
20
+ import { AiSession } from '../session';
18
21
 
19
22
  /**
20
23
  * Exec external service and return the results as a Subgraph.
@@ -34,56 +37,51 @@ export const researchFn = defineFunction({
34
37
  }),
35
38
  }),
36
39
  outputSchema: Schema.Struct({
37
- // result: Schema.String,
38
- result: Schema.Any,
40
+ result: Schema.Unknown,
39
41
  }),
40
- handler: async ({ data: { query, mockSearch }, context }) => {
41
- const ai = context.getService(AiService);
42
- const credentials = context.getService(CredentialsService);
43
- const { db } = context.getService(DatabaseService);
44
- // const queues = context.getService(QueueService);
45
- const tracing = context.getService(TracingService);
46
-
47
- tracing.write(create(AgentStatusReport, { message: 'Researching...' }));
48
-
49
- const exaCredential = await credentials.getCredential({ service: 'exa.ai' });
50
- const searchTool = mockSearch ? createMockExaTool() : createExaTool({ apiKey: exaCredential.apiKey! });
42
+ handler: Effect.fnUntraced(
43
+ function* ({ data: { query, mockSearch } }) {
44
+ yield* TracingService.emitStatus({ message: 'Researching...' });
51
45
 
52
- const printer = new ConsolePrinter();
53
- const session = new AISession({ operationModel: 'configured' });
54
- session.message.on((message) => printer.printMessage(message));
55
- session.userMessage.on((message) => printer.printMessage(message));
56
- session.block.on((block) => printer.printContentBlock(block));
57
- session.statusReport.on((status) => {
58
- log.info('[agent] status', { status });
59
- tracing.write(status);
60
- });
61
- session.streamEvent.on((event) => log('stream', { event }));
46
+ const graphWriteToolkit = makeGraphWriterToolkit({ schema: DataTypes });
47
+ const toolkit = yield* AiToolkit.merge(ExaToolkit, LocalSearchToolkit, graphWriteToolkit).pipe(
48
+ Effect.provide(
49
+ Layer.mergeAll(
50
+ mockSearch ? MockExaHandler : LiveExaHandler,
51
+ LocalSearchHandler,
52
+ makeGraphWriterHandler(graphWriteToolkit),
53
+ ),
54
+ ),
55
+ );
62
56
 
63
- const graphWriteTool = createGraphWriterTool({ db, schema: DataTypes });
64
- log.info('graphWriteTool', { schema: graphWriteTool.parameters });
57
+ const printer = new ConsolePrinter();
58
+ const session = new AiSession();
59
+ session.message.on((message) => printer.printMessage(message));
60
+ session.userMessage.on((message) => printer.printMessage(message));
61
+ session.block.on((block) => printer.printContentBlock(block));
62
+ session.streamEvent.on((event) => log('stream', { event }));
65
63
 
66
- // TODO(dmaretskyi): Consider adding this pattern as the "Graph" output mode for the session.
67
- const result = await session.run({
68
- client: ai.client,
69
- systemPrompt: PROMPT,
70
- artifacts: [],
71
- tools: [searchTool, graphWriteTool],
72
- history: [],
73
- prompt: query,
74
- });
64
+ // TODO(dmaretskyi): Consider adding this pattern as the "Graph" output mode for the session.
65
+ const result = yield* session.run({
66
+ prompt: query,
67
+ history: [],
68
+ systemPrompt: PROMPT,
69
+ toolkit,
70
+ });
75
71
 
76
- return {
77
- result: result as any,
78
- };
72
+ return {
73
+ result,
74
+ };
79
75
 
80
- // queues.contextQueue!.append(data);
76
+ // queues.contextQueue!.append(data);
81
77
 
82
- // return {
83
- // result: `
84
- // The research results are placed in the following objects:
85
- // ${data.map((object, id) => `[obj_${id}][dxn:echo:@:${object.id}]`).join('\n')}
86
- // `,
87
- // };
88
- },
78
+ // return {
79
+ // result: `
80
+ // The research results are placed in the following objects:
81
+ // ${data.map((object, id) => `[obj_${id}][dxn:echo:@:${object.id}]`).join('\n')}
82
+ // `,
83
+ // };
84
+ },
85
+ Effect.provide(AiService.model('@anthropic/claude-3-5-sonnet-20241022')),
86
+ ),
89
87
  });
@@ -0,0 +1,32 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`AiSession > no tools 1`] = `
4
+ [
5
+ {
6
+ "blocks": ReactiveArray [
7
+ {
8
+ "_tag": "text",
9
+ "text": "Hello world!",
10
+ },
11
+ ],
12
+ "created": "Sun, 20 Jul 2025 10:54:08 GMT",
13
+ "id": "01K0KQGFTFV3GBKGEWZW67K8HK",
14
+ "sender": {
15
+ "role": "user",
16
+ },
17
+ },
18
+ {
19
+ "blocks": ReactiveArray [
20
+ {
21
+ "_tag": "text",
22
+ "text": "Hi there! How can I help you today?",
23
+ },
24
+ ],
25
+ "created": "2025-07-20T10:54:10.801Z",
26
+ "id": "01K0KQGHVHJ37D4YG45WG090WV",
27
+ "sender": {
28
+ "role": "assistant",
29
+ },
30
+ },
31
+ ]
32
+ `;