@dxos/assistant 0.9.1-staging.ee54ba693a → 0.11.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.
Files changed (45) hide show
  1. package/dist/lib/extraction.mjs +169 -0
  2. package/dist/lib/extraction.mjs.map +1 -0
  3. package/dist/lib/index.mjs +1958 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/types/src/request/AiRequest.d.ts +6 -3
  6. package/dist/types/src/request/AiRequest.d.ts.map +1 -1
  7. package/dist/types/src/request/AiRequest.test.d.ts +2 -0
  8. package/dist/types/src/request/AiRequest.test.d.ts.map +1 -0
  9. package/dist/types/src/request/format.d.ts +2 -2
  10. package/dist/types/src/request/format.d.ts.map +1 -1
  11. package/dist/types/src/request/format.test.d.ts +2 -0
  12. package/dist/types/src/request/format.test.d.ts.map +1 -0
  13. package/dist/types/src/session/AiContext.d.ts +1 -1
  14. package/dist/types/src/session/AiContext.d.ts.map +1 -1
  15. package/dist/types/src/session/AiSession.d.ts +25 -4
  16. package/dist/types/src/session/AiSession.d.ts.map +1 -1
  17. package/dist/types/src/session/harness-control.d.ts +4 -0
  18. package/dist/types/src/session/harness-control.d.ts.map +1 -1
  19. package/dist/types/src/tool-runtime/services.d.ts +7 -0
  20. package/dist/types/src/tool-runtime/services.d.ts.map +1 -1
  21. package/dist/types/src/tool-runtime/services.test.d.ts +2 -0
  22. package/dist/types/src/tool-runtime/services.test.d.ts.map +1 -0
  23. package/dist/types/src/util/execution-graph.d.ts.map +1 -1
  24. package/dist/types/src/util/tracing.d.ts +10 -0
  25. package/dist/types/src/util/tracing.d.ts.map +1 -1
  26. package/dist/types/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +25 -25
  28. package/src/extraction/quotes.test.ts +4 -4
  29. package/src/request/AiRequest.test.ts +110 -0
  30. package/src/request/AiRequest.ts +11 -4
  31. package/src/request/format.test.ts +93 -0
  32. package/src/request/format.ts +48 -11
  33. package/src/session/AiContext.ts +1 -1
  34. package/src/session/AiSession.test.ts +124 -0
  35. package/src/session/AiSession.ts +81 -8
  36. package/src/tool-runtime/services.test.ts +41 -0
  37. package/src/tool-runtime/services.ts +16 -1
  38. package/src/util/execution-graph.ts +1 -1
  39. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  40. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  41. package/dist/lib/neutral/extraction/index.mjs +0 -197
  42. package/dist/lib/neutral/extraction/index.mjs.map +0 -7
  43. package/dist/lib/neutral/index.mjs +0 -2047
  44. package/dist/lib/neutral/index.mjs.map +0 -7
  45. package/dist/lib/neutral/meta.json +0 -1
@@ -4,22 +4,24 @@
4
4
 
5
5
  // @import-as-namespace
6
6
 
7
- import { type Registry as AtomRegistry } from '@effect-atom/atom-react';
7
+ import { type Registry as AtomRegistry } from '@effect-atom/atom';
8
8
  import * as Array from 'effect/Array';
9
9
  import * as Effect from 'effect/Effect';
10
10
  import * as Either from 'effect/Either';
11
11
  import { pipe } from 'effect/Function';
12
12
  import * as Layer from 'effect/Layer';
13
+ import * as Order from 'effect/Order';
13
14
  import * as Record from 'effect/Record';
14
15
  import * as Runtime from 'effect/Runtime';
15
16
 
16
17
  import { type OpaqueToolkit, type ToolExecutionService, type ToolResolverService } from '@dxos/ai';
17
- import { McpServer, Operation, type Skill, Trace } from '@dxos/compute';
18
+ import { type Instructions, McpServer, Operation, type Skill, Trace } from '@dxos/compute';
18
19
  import { Resource } from '@dxos/context';
19
20
  import { Database, Feed, Filter, Obj, Registry } from '@dxos/echo';
20
21
  import { invariant } from '@dxos/invariant';
21
22
  import { log } from '@dxos/log';
22
23
  import { McpToolkit } from '@dxos/mcp-client';
24
+ import { FeedProtocol } from '@dxos/protocols';
23
25
  import { type ContentBlock, Message } from '@dxos/types';
24
26
 
25
27
  import { AiRequest, type GenerationObserver, formatSystemPrompt } from '../request';
@@ -55,6 +57,11 @@ export type Options = {
55
57
  runtime: Runtime.Runtime<Database.Service>;
56
58
  /** @effect-atom/atom-react Registry for reactive state. */
57
59
  registry?: AtomRegistry.Registry;
60
+ /**
61
+ * Instructions steering the conversation (typically the owning `Chat`'s), rendered into the system
62
+ * prompt on every turn. The session is feed-centric and cannot reach its chat, so these are passed in.
63
+ */
64
+ instructions?: readonly Instructions.Instructions[];
58
65
  };
59
66
 
60
67
  /**
@@ -68,18 +75,22 @@ const SUMMARY_THRESHOLD = 80_000;
68
75
  * Executes tools based on AI responses and supports cancellation of in-progress requests.
69
76
  */
70
77
  export class Session extends Resource {
78
+ private readonly _feed: Feed.Feed;
79
+ private readonly _runtime: Runtime.Runtime<Database.Service>;
80
+ readonly #instructions: readonly Instructions.Instructions[];
81
+
71
82
  /**
72
83
  * Skills and objects bound to the session.
73
84
  */
74
85
  private readonly _binder: AiContext.Binder;
75
- private readonly _feed: Feed.Feed;
76
- private readonly _runtime: Runtime.Runtime<Database.Service>;
86
+
77
87
  private readonly _sessionLoader = new SessionLoader();
78
88
 
79
89
  public constructor(options: Options) {
80
90
  super();
81
91
  this._feed = options.feed;
82
92
  this._runtime = options.runtime;
93
+ this.#instructions = options.instructions ?? [];
83
94
  invariant(this._feed);
84
95
  invariant(this._runtime);
85
96
  this._binder = new AiContext.Binder({
@@ -101,11 +112,23 @@ export class Session extends Resource {
101
112
  return this._binder;
102
113
  }
103
114
 
115
+ /**
116
+ * The conversation as the model should see it: only the messages reachable from the feed's current
117
+ * head, so a soft fork's abandoned turns are excluded.
118
+ */
104
119
  public async getHistory(): Promise<Message.Message[]> {
120
+ const { items: reachable } = Feed.history(await this.#messagesInAppendOrder());
121
+ return Runtime.runPromise(this._runtime)(this._sessionLoader.reifyHistory(this._feed, reachable));
122
+ }
123
+
124
+ /**
125
+ * Every message in the feed, in append order — what `Feed.history` needs, since it walks lineage
126
+ * positionally rather than by `created`, a wall clock peers do not agree on.
127
+ */
128
+ async #messagesInAppendOrder(): Promise<Message.Message[]> {
105
129
  const queryResult = await Runtime.runPromise(this._runtime)(Feed.query(this._feed, Filter.type(Message.Message)));
106
130
  const items = await queryResult.run();
107
- const messages = items.filter(Obj.instanceOf(Message.Message));
108
- return Runtime.runPromise(this._runtime)(this._sessionLoader.reifyHistory(this._feed, messages));
131
+ return Array.sort(items.filter(Obj.instanceOf(Message.Message)), byFeedPosition);
109
132
  }
110
133
 
111
134
  getTools(): Effect.Effect<
@@ -132,6 +155,39 @@ export class Session extends Resource {
132
155
  );
133
156
  }
134
157
 
158
+ /**
159
+ * Appends one of this turn's messages to the feed, consuming a pending rewind if there is one.
160
+ *
161
+ * The rewind decision is made in the UI, but the continuation is appended by the agent's process,
162
+ * which resolves the feed and never sees the chat — so the feed carries the intent and this is where
163
+ * it becomes lineage. `rewindFrom` names the earliest discarded message, so the new parent is whatever
164
+ * precedes it; nothing preceding means the continuation starts a fresh line. Only the first message of
165
+ * a turn finds it set, since the append clears it, so the rest of the turn chains implicitly. Clearing
166
+ * after the append (rather than before) leaves the rewind pending if the turn fails.
167
+ */
168
+ public async appendTurnMessage(message: Message.Message): Promise<void> {
169
+ const rewindFrom = this._feed.rewindFrom;
170
+ const parent = rewindFrom !== undefined ? await this.#parentForRewind(rewindFrom) : undefined;
171
+
172
+ return Runtime.runPromise(this._runtime)(
173
+ Effect.gen(this, function* () {
174
+ yield* Feed.append(this._feed, [message], parent !== undefined ? { parent } : undefined);
175
+ if (rewindFrom !== undefined) {
176
+ Obj.update(this._feed, (feed) => {
177
+ feed.rewindFrom = undefined;
178
+ });
179
+ }
180
+ }),
181
+ );
182
+ }
183
+
184
+ /** The message preceding `rewindFrom` in append order, which the continuation parents to. */
185
+ async #parentForRewind(rewindFrom: string): Promise<string | undefined> {
186
+ const messages = await this.#messagesInAppendOrder();
187
+ const index = messages.findIndex((message) => message.id === rewindFrom);
188
+ return index > 0 ? messages[index - 1].id : undefined;
189
+ }
190
+
135
191
  /**
136
192
  * Creates a new cancelable request effect.
137
193
  */
@@ -153,14 +209,14 @@ export class Session extends Resource {
153
209
  summarizationThreshold: SUMMARY_THRESHOLD,
154
210
  observer: params.observer,
155
211
  persist: params.persist,
156
- onOutput: (message) =>
157
- Effect.promise(() => Runtime.runPromise(this._runtime)(Feed.append(this._feed, [message]))),
212
+ onOutput: (message) => Effect.promise(() => this.appendTurnMessage(message)),
158
213
  });
159
214
 
160
215
  yield* request.begin({
161
216
  history,
162
217
  skills,
163
218
  objects,
219
+ instructions: this.#instructions,
164
220
  prompt: params.prompt,
165
221
  system: params.system,
166
222
  });
@@ -189,6 +245,7 @@ export class Session extends Resource {
189
245
  system: params.system,
190
246
  skills: currentSkills,
191
247
  objects: this.context.getObjects(),
248
+ instructions: this.#instructions,
192
249
  }).pipe(Effect.orDie);
193
250
 
194
251
  const { done, finishReason } = yield* request.runAgentTurn({ system, toolkit });
@@ -294,3 +351,19 @@ const connectMcpServers = (
294
351
  Effect.map(Array.filterMap((_) => Either.getRight(_))),
295
352
  );
296
353
  };
354
+
355
+ /**
356
+ * Orders feed items by the position the server assigned them. Unpositioned blocks (written locally and
357
+ * not yet acknowledged) sort last, which is what we want: a message just written is the newest.
358
+ */
359
+ const byFeedPosition = Order.make<Message.Message>((a, b) => {
360
+ const positionA = feedPosition(a);
361
+ const positionB = feedPosition(b);
362
+ return positionA === positionB ? 0 : positionA < positionB ? -1 : 1;
363
+ });
364
+
365
+ const feedPosition = (message: Message.Message): number => {
366
+ const key = Obj.getKeys(message, FeedProtocol.KEY_QUEUE_POSITION).at(0)?.id;
367
+ const position = key !== undefined ? Number(key) : Number.NaN;
368
+ return Number.isNaN(position) ? Number.POSITIVE_INFINITY : position;
369
+ };
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+ import { describe, test } from 'vitest';
7
+
8
+ import { Obj, Ref } from '@dxos/echo';
9
+ import { EID, EntityId, SpaceId } from '@dxos/keys';
10
+
11
+ import { createStructFieldsFromSchema } from './services';
12
+
13
+ describe('createStructFieldsFromSchema', () => {
14
+ const SPACE = SpaceId.random();
15
+ const OBJECT = EntityId.random();
16
+
17
+ // Projects a tool input schema for the LLM and decodes the given `in` value the way a tool call would.
18
+ const decodeIn = (schema: Schema.Schema<any, any>, value: unknown): any[] => {
19
+ const fields = createStructFieldsFromSchema(schema);
20
+ const decoded: any = Schema.decodeUnknownSync(Schema.Struct(fields))({ in: value });
21
+ return decoded.in;
22
+ };
23
+
24
+ // An LLM-supplied ref URI string must coerce to a valid local EID. Refs nested inside an optional
25
+ // field surface as a `T | undefined` union; the projection must still route them through the
26
+ // LLM-friendly coercion so a qualified URI does not become a malformed `echo:////…`.
27
+ test('coerces refs passed as qualified URI strings inside an optional array', ({ expect }) => {
28
+ const refs = decodeIn(Schema.Struct({ in: Schema.optional(Schema.Array(Ref.Ref(Obj.Unknown))) }), [
29
+ `echo://${SPACE}/${OBJECT}`,
30
+ ]);
31
+ expect(refs).toHaveLength(1);
32
+ expect(() => EID.parse(refs[0].uri)).not.toThrow();
33
+ expect(refs[0].uri).toBe(`echo:///${OBJECT}`);
34
+ });
35
+
36
+ test('coerces refs passed as qualified URI strings inside a required array', ({ expect }) => {
37
+ const refs = decodeIn(Schema.Struct({ in: Schema.Array(Ref.Ref(Obj.Unknown)) }), [`echo://${SPACE}/${OBJECT}`]);
38
+ expect(refs).toHaveLength(1);
39
+ expect(refs[0].uri).toBe(`echo:///${OBJECT}`);
40
+ });
41
+ });
@@ -173,7 +173,14 @@ const makeToolName = (name: string) => {
173
173
  };
174
174
 
175
175
  // TODO(dmaretskyi): Factor out.
176
- const createStructFieldsFromSchema = (schema: Schema.Schema<any, any>): Record<string, Schema.Schema<any, any>> => {
176
+ /**
177
+ * Projects an operation input struct into the LLM-facing tool parameter fields, mapping each
178
+ * property through {@link mapSchemaTypeForLLM} (so refs become the model-friendly `RefFromLLM`).
179
+ * Exported for testing.
180
+ */
181
+ export const createStructFieldsFromSchema = (
182
+ schema: Schema.Schema<any, any>,
183
+ ): Record<string, Schema.Schema<any, any>> => {
177
184
  switch (schema.ast._tag) {
178
185
  case 'TypeLiteral':
179
186
  return Object.fromEntries(
@@ -218,6 +225,14 @@ const mapSchemaTypeForLLM = (ast: SchemaAST.AST): SchemaAST.AST => {
218
225
  ast.indexSignatures,
219
226
  ast.annotations,
220
227
  );
228
+ } else if (SchemaAST.isUnion(ast)) {
229
+ // `Schema.optional(Schema.Array(Ref(...)))` surfaces the value as a `T | undefined` union, so a
230
+ // ref nested inside an optional/union field must be mapped through each member — otherwise it
231
+ // keeps the raw `Ref` encoding and an LLM-supplied URI string is never coerced via `RefFromLLM`.
232
+ return SchemaAST.Union.make(
233
+ ast.types.map((member) => mapSchemaTypeForLLM(member)),
234
+ ast.annotations,
235
+ );
221
236
  }
222
237
 
223
238
  return ast;
@@ -3,8 +3,8 @@
3
3
  //
4
4
 
5
5
  import { AgentStatus } from '@dxos/ai';
6
+ import { MESSAGE_PROPERTY_TOOL_CALL_ID } from '@dxos/compute-runtime';
6
7
  import { type Entity, Obj, type Ref } from '@dxos/echo';
7
- import { MESSAGE_PROPERTY_TOOL_CALL_ID } from '@dxos/functions';
8
8
  import { EID, type EntityId } from '@dxos/keys';
9
9
  import { LogLevel } from '@dxos/log';
10
10
  import { ContentBlock, Message } from '@dxos/types';
@@ -1,10 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
6
-
7
- export {
8
- __export
9
- };
10
- //# sourceMappingURL=chunk-J5LGTIGS.mjs.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": [],
4
- "sourcesContent": [],
5
- "mappings": "",
6
- "names": []
7
- }
@@ -1,197 +0,0 @@
1
- import "../chunk-J5LGTIGS.mjs";
2
-
3
- // src/extraction/extraction-llm-function/proper-noun-extraction.ts
4
- import * as LanguageModel from "@effect/ai/LanguageModel";
5
- import * as Effect from "effect/Effect";
6
- import * as Schema2 from "effect/Schema";
7
- import { AiService } from "@dxos/ai";
8
- import { Obj } from "@dxos/echo";
9
- import { Message } from "@dxos/types";
10
-
11
- // src/extraction/quotes.ts
12
- import * as Schema from "effect/Schema";
13
- import { Filter, Query } from "@dxos/echo";
14
- import { EID, EntityId } from "@dxos/keys";
15
- var ReferencedQuotes = Schema.Struct({
16
- references: Schema.Array(Schema.Struct({
17
- id: Schema.String,
18
- quote: Schema.String
19
- })).pipe(Schema.mutable).annotations({
20
- // TODO(burdon): Does this description make sense?
21
- description: `
22
- The references to the context objects that are mentioned in the transcript.
23
- Quote should match the original transcript text exactly, while id is the id of the context object.
24
- `
25
- })
26
- });
27
- var findReferences = async (nouns, db, { searchKind = "full-text" } = {}) => {
28
- const normalizedNouns = Array.from(new Set(nouns.map((noun) => noun.trim()).filter((noun) => noun.length > 0))).sort((leftNoun, rightNoun) => rightNoun.length - leftNoun.length);
29
- const references = await Promise.all(normalizedNouns.map(async (noun) => {
30
- const objects = await db.query(Query.select(Filter.text(noun, {
31
- type: searchKind
32
- }))).run();
33
- return objects.length > 0 ? [
34
- {
35
- id: objects[0].id.toString(),
36
- quote: noun
37
- }
38
- ] : [];
39
- }));
40
- return {
41
- references: references.flat()
42
- };
43
- };
44
- var insertReferences = (text, quotes) => {
45
- const references = quotes.references.filter((quote) => EntityId.isValid(quote.id)).sort((left, right) => right.quote.length - left.quote.length);
46
- for (const quote of references) {
47
- const regex = new RegExp(quote.quote.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
48
- text = replaceOutsideLinks(text, regex, `[${quote.quote}](${EID.make({
49
- entityId: quote.id
50
- })})`);
51
- }
52
- return text;
53
- };
54
- var MARKDOWN_LINK = /\[[^\]]*\]\([^)]*\)/g;
55
- var replaceOutsideLinks = (text, regex, replacement) => {
56
- let result = "";
57
- let plainStart = 0;
58
- for (const link of text.matchAll(MARKDOWN_LINK)) {
59
- const index = link.index ?? 0;
60
- result += text.slice(plainStart, index).replace(regex, replacement) + link[0];
61
- plainStart = index + link[0].length;
62
- }
63
- return result + text.slice(plainStart).replace(regex, replacement);
64
- };
65
-
66
- // src/extraction/extraction-llm-function/proper-noun-extraction.ts
67
- var EXTRACTION_MODEL = "com.anthropic.model.claude-haiku-4-5.default";
68
- var ProperNouns = Schema2.Struct({
69
- properNouns: Schema2.Array(Schema2.String).annotations({
70
- description: "Proper nouns (names of people, organizations, places, products) mentioned in the text."
71
- })
72
- });
73
- var extractProperNouns = (text) => Effect.gen(function* () {
74
- const { value } = yield* Effect.scoped(LanguageModel.generateObject({
75
- schema: ProperNouns,
76
- prompt: [
77
- "Extract every proper noun (people, organizations, places, products) mentioned in the transcript text below.",
78
- "Return only the surface strings exactly as they appear. Exclude common nouns and pronouns.",
79
- "",
80
- "Transcript:",
81
- text
82
- ].join("\n")
83
- }));
84
- return value.properNouns.map((noun) => noun.trim()).filter((noun) => noun.length >= 3);
85
- }).pipe(Effect.provide(AiService.model(EXTRACTION_MODEL)));
86
- var enrichTranscriptMessage = (message, { db }) => Effect.gen(function* () {
87
- const blocks = yield* Effect.forEach(message.blocks, (block) => Effect.gen(function* () {
88
- if (block._tag !== "transcript") {
89
- return block;
90
- }
91
- const nouns = yield* extractProperNouns(block.text);
92
- const references = yield* Effect.promise(() => findReferences([
93
- ...nouns
94
- ], db));
95
- return {
96
- ...block,
97
- text: insertReferences(block.text, references)
98
- };
99
- }));
100
- return Obj.make(Message.Message, {
101
- id: message.id,
102
- sender: message.sender,
103
- created: message.created,
104
- properties: message.properties,
105
- blocks
106
- });
107
- });
108
-
109
- // src/extraction/extraction-ner-function/named-entity-recognition.ts
110
- import { pipeline } from "@xenova/transformers";
111
- import { log } from "@dxos/log";
112
- var __dxlog_file = "/__w/dxos/dxos/packages/core/compute/assistant/src/extraction/extraction-ner-function/named-entity-recognition.ts";
113
- var _ner;
114
- var getNer = () => {
115
- if (!_ner) {
116
- _ner = pipeline("ner", "Xenova/bert-base-NER").then((ner) => {
117
- log.info("NER model is ready", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 14, S: void 0 });
118
- return ner;
119
- });
120
- }
121
- return _ner;
122
- };
123
- var extractFullEntities = async (text) => {
124
- const ner = await getNer();
125
- const entities = await ner(text);
126
- const tokenGroups = createTokenGroups(entities);
127
- const combinedEntities = tokenGroups.map(combineNerTokens);
128
- return combinedEntities;
129
- };
130
- var createTokenGroups = (tokens) => {
131
- const tokenGroups = [];
132
- const canBeCombined = (prevToken, currentToken) => {
133
- const [currentState, currentEntity] = currentToken.entity.split("-");
134
- const [prevState, prevEntity] = prevToken.entity.split("-");
135
- if (prevEntity !== currentEntity) {
136
- return false;
137
- }
138
- if (prevState === "O") {
139
- return false;
140
- }
141
- if (currentState === "I") {
142
- return true;
143
- }
144
- if ((prevState === "B" || prevState === "I") && currentState === "B") {
145
- return currentToken.word.startsWith("#");
146
- }
147
- return false;
148
- };
149
- for (const token of tokens) {
150
- if (tokenGroups.length === 0) {
151
- tokenGroups.push([
152
- token
153
- ]);
154
- continue;
155
- }
156
- const lastTokenGroup = tokenGroups.at(-1);
157
- if (canBeCombined(lastTokenGroup.at(-1), token)) {
158
- lastTokenGroup.push(token);
159
- continue;
160
- }
161
- tokenGroups.push([
162
- token
163
- ]);
164
- }
165
- return tokenGroups;
166
- };
167
- var combineNerTokens = (group) => {
168
- const combinedToken = {
169
- entity: group.at(0).entity.split("-")[1],
170
- score: group.reduce((acc, token) => acc + token.score, 0) / group.length,
171
- index: group.at(0).index,
172
- word: group.at(0).word,
173
- start: group.at(0).start,
174
- end: group.at(-1).end
175
- };
176
- for (const token of group.slice(1)) {
177
- if (token.word.startsWith("#")) {
178
- combinedToken.word += token.word.replace(/^#+/, "");
179
- } else {
180
- combinedToken.word += " " + token.word;
181
- }
182
- }
183
- return combinedToken;
184
- };
185
- export {
186
- ProperNouns,
187
- ReferencedQuotes,
188
- combineNerTokens,
189
- createTokenGroups,
190
- enrichTranscriptMessage,
191
- extractFullEntities,
192
- extractProperNouns,
193
- findReferences,
194
- getNer,
195
- insertReferences
196
- };
197
- //# sourceMappingURL=index.mjs.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/extraction/extraction-llm-function/proper-noun-extraction.ts", "../../../../src/extraction/quotes.ts", "../../../../src/extraction/extraction-ner-function/named-entity-recognition.ts"],
4
- "sourcesContent": ["//\n// Copyright 2025 DXOS.org\n//\n\nimport * as LanguageModel from '@effect/ai/LanguageModel';\nimport * as Effect from 'effect/Effect';\nimport * as Schema from 'effect/Schema';\n\nimport { AiService } from '@dxos/ai';\nimport { type Database, Obj } from '@dxos/echo';\nimport { Message } from '@dxos/types';\n\nimport { findReferences, insertReferences } from '../quotes';\n\nconst EXTRACTION_MODEL = 'com.anthropic.model.claude-haiku-4-5.default';\n\n/**\n * Proper nouns extracted from transcript text.\n */\nexport const ProperNouns = Schema.Struct({\n properNouns: Schema.Array(Schema.String).annotations({\n description: 'Proper nouns (names of people, organizations, places, products) mentioned in the text.',\n }),\n});\nexport type ProperNouns = Schema.Schema.Type<typeof ProperNouns>;\n\n/**\n * Extracts proper nouns from transcript text using a small LLM (Haiku). Provides the LanguageModel\n * internally so the residual requirement is just {@link AiService.AiService}.\n */\nexport const extractProperNouns = (text: string) =>\n Effect.gen(function* () {\n const { value } = yield* Effect.scoped(\n LanguageModel.generateObject({\n schema: ProperNouns,\n prompt: [\n 'Extract every proper noun (people, organizations, places, products) mentioned in the transcript text below.',\n 'Return only the surface strings exactly as they appear. Exclude common nouns and pronouns.',\n '',\n 'Transcript:',\n text,\n ].join('\\n'),\n }),\n );\n\n // Drop short tokens (e.g. \"IT\"): insertReferences replaces case-insensitive substrings, so a\n // short noun would match inside unrelated words (\"secur[IT]y\") and corrupt neighbouring links.\n return value.properNouns.map((noun) => noun.trim()).filter((noun) => noun.length >= 3);\n }).pipe(Effect.provide(AiService.model(EXTRACTION_MODEL)));\n\n/**\n * Enriches a transcript message: for each transcript block, extracts proper nouns, links them to\n * objects in the space via full-text search, and rewrites the text with `[noun](dxn)` references.\n * Requires {@link AiService.AiService}; takes the space database handle.\n */\nexport const enrichTranscriptMessage = (message: Message.Message, { db }: { db: Database.Database }) =>\n Effect.gen(function* () {\n const blocks = yield* Effect.forEach(message.blocks, (block) =>\n Effect.gen(function* () {\n if (block._tag !== 'transcript') {\n return block;\n }\n\n const nouns = yield* extractProperNouns(block.text);\n const references = yield* Effect.promise(() => findReferences([...nouns], db));\n return { ...block, text: insertReferences(block.text, references) };\n }),\n );\n\n // Preserve the message identity and metadata; enrichment only rewrites transcript block text.\n return Obj.make(Message.Message, {\n id: message.id,\n sender: message.sender,\n created: message.created,\n properties: message.properties,\n blocks,\n });\n });\n", "//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Schema from 'effect/Schema';\n\nimport { type Database, Filter, Query } from '@dxos/echo';\nimport { EID, EntityId } from '@dxos/keys';\n\nexport const ReferencedQuotes = Schema.Struct({\n references: Schema.Array(\n Schema.Struct({\n id: Schema.String,\n quote: Schema.String, // TODO(burdon): Quote?\n }),\n )\n .pipe(Schema.mutable)\n .annotations({\n // TODO(burdon): Does this description make sense?\n description: `\n The references to the context objects that are mentioned in the transcript. \n Quote should match the original transcript text exactly, while id is the id of the context object.\n `,\n }),\n});\nexport type ReferencedQuotes = Schema.Schema.Type<typeof ReferencedQuotes>;\n\nexport type FindReferencesOptions = {\n /** Search backend. Full-text matches proper nouns precisely; vector matches semantically. */\n searchKind?: 'full-text' | 'vector';\n};\n\n/**\n * Finds the best-matching context object for each noun and returns it as a reference.\n */\nexport const findReferences = async (\n nouns: string[],\n db: Database.Database,\n { searchKind = 'full-text' }: FindReferencesOptions = {},\n): Promise<ReferencedQuotes> => {\n // Deduplicate and process longer nouns first: `insertReferences` rewrites case-insensitive\n // substrings, so a repeated or shorter overlapping noun would re-replace already-linked text and\n // produce nested/broken markdown links.\n const normalizedNouns = Array.from(new Set(nouns.map((noun) => noun.trim()).filter((noun) => noun.length > 0))).sort(\n (leftNoun, rightNoun) => rightNoun.length - leftNoun.length,\n );\n\n const references = await Promise.all(\n normalizedNouns.map(async (noun) => {\n const objects = await db.query(Query.select(Filter.text(noun, { type: searchKind }))).run();\n return objects.length > 0 ? [{ id: objects[0].id.toString(), quote: noun }] : [];\n }),\n );\n return { references: references.flat() };\n};\n\n/**\n * Finds and replaces all quotes with DXNs references.\n */\n// TODO(dmaretskyi): Lookup and verifiy ids from provided context.\nexport const insertReferences = (text: string, quotes: ReferencedQuotes): string => {\n // Process longer quotes first so e.g. \"Sarah Johnson\" is linked before \"Sarah\". Combined with the\n // link-aware replacement below this stops a shorter or overlapping quote from rewriting text that\n // is already part of an inserted link (which would produce nested `[[..](..)](..)` markdown).\n const references = quotes.references\n .filter((quote) => EntityId.isValid(quote.id))\n .sort((left, right) => right.quote.length - left.quote.length);\n\n for (const quote of references) {\n // Case-insensitive match of the (regex-escaped) quote.\n const regex = new RegExp(quote.quote.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi');\n text = replaceOutsideLinks(text, regex, `[${quote.quote}](${EID.make({ entityId: quote.id })})`);\n }\n\n return text;\n};\n\nconst MARKDOWN_LINK = /\\[[^\\]]*\\]\\([^)]*\\)/g;\n\n/** Applies a replacement to every span of `text` that is not already inside a `[label](url)` link. */\nconst replaceOutsideLinks = (text: string, regex: RegExp, replacement: string): string => {\n let result = '';\n let plainStart = 0;\n for (const link of text.matchAll(MARKDOWN_LINK)) {\n const index = link.index ?? 0;\n result += text.slice(plainStart, index).replace(regex, replacement) + link[0];\n plainStart = index + link[0].length;\n }\n return result + text.slice(plainStart).replace(regex, replacement);\n};\n", "//\n// Copyright 2025 DXOS.org\n//\n\nimport {\n type TokenClassificationOutput,\n type TokenClassificationPipelineType,\n type TokenClassificationSingle,\n pipeline,\n} from '@xenova/transformers';\n\nimport { log } from '@dxos/log';\n\nlet _ner: Promise<TokenClassificationPipelineType>;\n\n/**\n * Named Entity Recognition pipeline.\n * Initializes the pipeline on first call.\n * @returns The singleton promise that resolves to a token classification pipeline.\n */\nexport const getNer = () => {\n if (!_ner) {\n _ner = pipeline('ner', 'Xenova/bert-base-NER').then((ner) => {\n log.info('NER model is ready');\n return ner;\n });\n }\n\n return _ner;\n};\n\n/**\n * Extracts entities from text using the named entity recognition pipeline and does simple grouping of tokens.\n */\nexport const extractFullEntities = async (text: string): Promise<TokenClassificationSingle[]> => {\n const ner = await getNer();\n const entities = (await ner(text)) as TokenClassificationOutput;\n const tokenGroups = createTokenGroups(entities);\n const combinedEntities = tokenGroups.map(combineNerTokens);\n return combinedEntities;\n};\n\nexport const createTokenGroups = (tokens: TokenClassificationSingle[]): TokenClassificationSingle[][] => {\n const tokenGroups: TokenClassificationSingle[][] = [];\n\n const canBeCombined = (prevToken: TokenClassificationSingle, currentToken: TokenClassificationSingle) => {\n const [currentState, currentEntity] = currentToken.entity.split('-'); // B, I, O\n const [prevState, prevEntity] = prevToken.entity.split('-'); // B, I, O\n if (prevEntity !== currentEntity) {\n return false;\n }\n if (prevState === 'O') {\n return false;\n }\n if (currentState === 'I') {\n return true;\n }\n if ((prevState === 'B' || prevState === 'I') && currentState === 'B') {\n return currentToken.word.startsWith('#');\n }\n return false;\n };\n\n for (const token of tokens) {\n if (tokenGroups.length === 0) {\n tokenGroups.push([token]);\n continue;\n }\n const lastTokenGroup = tokenGroups.at(-1)!;\n if (canBeCombined(lastTokenGroup.at(-1)!, token)) {\n lastTokenGroup.push(token);\n continue;\n }\n tokenGroups.push([token]);\n }\n\n return tokenGroups;\n};\n\nexport const combineNerTokens = (group: TokenClassificationSingle[]): TokenClassificationSingle => {\n const combinedToken: TokenClassificationSingle = {\n entity: group.at(0)!.entity.split('-')[1],\n score: group.reduce((acc, token) => acc + token.score, 0) / group.length,\n index: group.at(0)!.index,\n word: group.at(0)!.word,\n start: group.at(0)!.start,\n end: group.at(-1)!.end,\n };\n\n for (const token of group.slice(1)) {\n if (token.word.startsWith('#')) {\n combinedToken.word += token.word.replace(/^#+/, '');\n } else {\n combinedToken.word += ' ' + token.word;\n }\n }\n\n return combinedToken;\n};\n"],
5
- "mappings": ";;;AAIA,YAAYA,mBAAmB;AAC/B,YAAYC,YAAY;AACxB,YAAYC,aAAY;AAExB,SAASC,iBAAiB;AAC1B,SAAwBC,WAAW;AACnC,SAASC,eAAe;;;ACNxB,YAAYC,YAAY;AAExB,SAAwBC,QAAQC,aAAa;AAC7C,SAASC,KAAKC,gBAAgB;AAEvB,IAAMC,mBAA0BC,cAAO;EAC5CC,YAAmBC,aACVF,cAAO;IACZG,IAAWC;IACXC,OAAcD;EAChB,CAAA,CAAA,EAECE,KAAYC,cAAO,EACnBC,YAAY;;IAEXC,aAAa;;;;EAIf,CAAA;AACJ,CAAA;AAWO,IAAMC,iBAAiB,OAC5BC,OACAC,IACA,EAAEC,aAAa,YAAW,IAA4B,CAAC,MAAC;AAKxD,QAAMC,kBAAkBZ,MAAMa,KAAK,IAAIC,IAAIL,MAAMM,IAAI,CAACC,SAASA,KAAKC,KAAI,CAAA,EAAIC,OAAO,CAACF,SAASA,KAAKG,SAAS,CAAA,CAAA,CAAA,EAAKC,KAC9G,CAACC,UAAUC,cAAcA,UAAUH,SAASE,SAASF,MAAM;AAG7D,QAAMpB,aAAa,MAAMwB,QAAQC,IAC/BZ,gBAAgBG,IAAI,OAAOC,SAAAA;AACzB,UAAMS,UAAU,MAAMf,GAAGgB,MAAMhC,MAAMiC,OAAOlC,OAAOmC,KAAKZ,MAAM;MAAEa,MAAMlB;IAAW,CAAA,CAAA,CAAA,EAAKmB,IAAG;AACzF,WAAOL,QAAQN,SAAS,IAAI;MAAC;QAAElB,IAAIwB,QAAQ,CAAA,EAAGxB,GAAG8B,SAAQ;QAAI5B,OAAOa;MAAK;QAAK,CAAA;EAChF,CAAA,CAAA;AAEF,SAAO;IAAEjB,YAAYA,WAAWiC,KAAI;EAAG;AACzC;AAMO,IAAMC,mBAAmB,CAACL,MAAcM,WAAAA;AAI7C,QAAMnC,aAAamC,OAAOnC,WACvBmB,OAAO,CAACf,UAAUP,SAASuC,QAAQhC,MAAMF,EAAE,CAAA,EAC3CmB,KAAK,CAACgB,MAAMC,UAAUA,MAAMlC,MAAMgB,SAASiB,KAAKjC,MAAMgB,MAAM;AAE/D,aAAWhB,SAASJ,YAAY;AAE9B,UAAMuC,QAAQ,IAAIC,OAAOpC,MAAMA,MAAMqC,QAAQ,uBAAuB,MAAA,GAAS,IAAA;AAC7EZ,WAAOa,oBAAoBb,MAAMU,OAAO,IAAInC,MAAMA,KAAK,KAAKR,IAAI+C,KAAK;MAAEC,UAAUxC,MAAMF;IAAG,CAAA,CAAA,GAAK;EACjG;AAEA,SAAO2B;AACT;AAEA,IAAMgB,gBAAgB;AAGtB,IAAMH,sBAAsB,CAACb,MAAcU,OAAeO,gBAAAA;AACxD,MAAIC,SAAS;AACb,MAAIC,aAAa;AACjB,aAAWC,QAAQpB,KAAKqB,SAASL,aAAAA,GAAgB;AAC/C,UAAMM,QAAQF,KAAKE,SAAS;AAC5BJ,cAAUlB,KAAKuB,MAAMJ,YAAYG,KAAAA,EAAOV,QAAQF,OAAOO,WAAAA,IAAeG,KAAK,CAAA;AAC3ED,iBAAaG,QAAQF,KAAK,CAAA,EAAG7B;EAC/B;AACA,SAAO2B,SAASlB,KAAKuB,MAAMJ,UAAAA,EAAYP,QAAQF,OAAOO,WAAAA;AACxD;;;AD3EA,IAAMO,mBAAmB;AAKlB,IAAMC,cAAqBC,eAAO;EACvCC,aAAoBC,cAAaC,cAAM,EAAEC,YAAY;IACnDC,aAAa;EACf,CAAA;AACF,CAAA;AAOO,IAAMC,qBAAqB,CAACC,SAC1BC,WAAI,aAAA;AACT,QAAM,EAAEC,MAAK,IAAK,OAAcC,cAChBC,6BAAe;IAC3BC,QAAQb;IACRc,QAAQ;MACN;MACA;MACA;MACA;MACAN;MACAO,KAAK,IAAA;EACT,CAAA,CAAA;AAKF,SAAOL,MAAMR,YAAYc,IAAI,CAACC,SAASA,KAAKC,KAAI,CAAA,EAAIC,OAAO,CAACF,SAASA,KAAKG,UAAU,CAAA;AACtF,CAAA,EAAGC,KAAYC,eAAQC,UAAUC,MAAMzB,gBAAAA,CAAAA,CAAAA;AAOlC,IAAM0B,0BAA0B,CAACC,SAA0B,EAAEC,GAAE,MAC7DlB,WAAI,aAAA;AACT,QAAMmB,SAAS,OAAcC,eAAQH,QAAQE,QAAQ,CAACE,UAC7CrB,WAAI,aAAA;AACT,QAAIqB,MAAMC,SAAS,cAAc;AAC/B,aAAOD;IACT;AAEA,UAAME,QAAQ,OAAOzB,mBAAmBuB,MAAMtB,IAAI;AAClD,UAAMyB,aAAa,OAAcC,eAAQ,MAAMC,eAAe;SAAIH;OAAQL,EAAAA,CAAAA;AAC1E,WAAO;MAAE,GAAGG;MAAOtB,MAAM4B,iBAAiBN,MAAMtB,MAAMyB,UAAAA;IAAY;EACpE,CAAA,CAAA;AAIF,SAAOI,IAAIC,KAAKC,QAAQA,SAAS;IAC/BC,IAAId,QAAQc;IACZC,QAAQf,QAAQe;IAChBC,SAAShB,QAAQgB;IACjBC,YAAYjB,QAAQiB;IACpBf;EACF,CAAA;AACF,CAAA;;;AEzEF,SAIEgB,gBACK;AAEP,SAASC,WAAW;AAEpB,IAAIC,eAAAA;;AASAA,IAAOF,SAAS,MAAA;aACdC;WACA,SAAOE,OAAAA,sBAAAA,EAAAA,KAAAA,CAAAA,QAAAA;AACT,UAAA,KAAA,sBAAA,QAAA,EAAA,YAAA,YAAA,GAAA,cAAA,GAAA,IAAA,GAAA,OAAA,CAAA;AACF,aAAA;IAEA,CAAA;EACA;AAEF,SAAA;;AAKQC,IAAAA,sBAAsBC,OAAAA,SAAAA;AAC5B,QAAMC,MAAAA,MAAAA,OAAcC;AACpB,QAAMC,WAAAA,MAAAA,IAAmBF,IAAAA;AACzB,QAAA,cAAOE,kBAAAA,QAAAA;AACP,QAAA,mBAAA,YAAA,IAAA,gBAAA;AAEF,SAAO;;AAGL,IAAMC,oBAAiBC,CAAAA,WAAsCC;QAC3D,cAAOC,CAAAA;QACP,gBAAkBC,CAAAA,WAAW,iBAAaC;AAC1C,UAAID,CAAAA,cAAeE,aAAe,IAAA,aAAA,OAAA,MAAA,GAAA;UAChC,CAAA,WAAO,UAAA,IAAA,UAAA,OAAA,MAAA,GAAA;AACT,QAAA,eAAA,eAAA;AACIC,aAAAA;;AAEJ,QAAA,cAAA,KAAA;AACIJ,aAAAA;;AAEJ,QAAA,iBAAA,KAAA;AACKI,aAAAA;;AAEL,SAAA,cAAA,OAAA,cAAA,QAAA,iBAAA,KAAA;AACA,aAAO,aAAA,KAAA,WAAA,GAAA;IACT;AAEA,WAAWC;;aAEPX,SAAYY,QAAK;oBAACD,WAAAA,GAAAA;kBAAM,KAAA;QACxB;MACF,CAAA;AACA;IACA;UACEE,iBAAmB,YAACF,GAAAA,EAAAA;QACpB,cAAA,eAAA,GAAA,EAAA,GAAA,KAAA,GAAA;AACF,qBAAA,KAAA,KAAA;AACAX;;gBAAwB,KAAA;MAC1B;IAEA,CAAA;EACA;AAEF,SAAO;;IAEHQ,mBAAqBA,CAAAA,UAAa;QAClCM,gBAAaC;IACbC,QAAOC,MAAMC,GAAG,CAAA,EAAIF,OAAK,MAAA,GAAA,EAAA,CAAA;IACzBG,OAAMF,MAAMC,OAAOC,CAAAA,KAAI,UAAA,MAAA,MAAA,OAAA,CAAA,IAAA,MAAA;IACvBC,OAAOH,MAAMC,GAAG,CAAA,EAAIE;IACpBC,MAAKJ,MAAMC,GAAI,CAAA,EAAIG;IACrB,OAAA,MAAA,GAAA,CAAA,EAAA;IAEA,KAAK,MAAMV,GAAAA,EAASM,EAAAA;;aAEhBK,SAAAA,MAAkB,MAAIX,CAAAA,GAAAA;AACxB,QAAA,MAAO,KAAA,WAAA,GAAA,GAAA;AACLW,oBAAcH,QAAQ,MAAMR,KAAAA,QAAU,OAAA,EAAA;IACxC,OAAA;AACF,oBAAA,QAAA,MAAA,MAAA;IAEA;EACA;;;",
6
- "names": ["LanguageModel", "Effect", "Schema", "AiService", "Obj", "Message", "Schema", "Filter", "Query", "EID", "EntityId", "ReferencedQuotes", "Struct", "references", "Array", "id", "String", "quote", "pipe", "mutable", "annotations", "description", "findReferences", "nouns", "db", "searchKind", "normalizedNouns", "from", "Set", "map", "noun", "trim", "filter", "length", "sort", "leftNoun", "rightNoun", "Promise", "all", "objects", "query", "select", "text", "type", "run", "toString", "flat", "insertReferences", "quotes", "isValid", "left", "right", "regex", "RegExp", "replace", "replaceOutsideLinks", "make", "entityId", "MARKDOWN_LINK", "replacement", "result", "plainStart", "link", "matchAll", "index", "slice", "EXTRACTION_MODEL", "ProperNouns", "Struct", "properNouns", "Array", "String", "annotations", "description", "extractProperNouns", "text", "gen", "value", "scoped", "generateObject", "schema", "prompt", "join", "map", "noun", "trim", "filter", "length", "pipe", "provide", "AiService", "model", "enrichTranscriptMessage", "message", "db", "blocks", "forEach", "block", "_tag", "nouns", "references", "promise", "findReferences", "insertReferences", "Obj", "make", "Message", "id", "sender", "created", "properties", "pipeline", "log", "_ner", "ner", "entities", "text", "tokenGroups", "createTokenGroups", "combinedEntities", "canBeCombined", "prevToken", "currentToken", "currentState", "prevEntity", "entity", "currentEntity", "prevState", "token", "push", "lastTokenGroup", "score", "reduce", "index", "group", "at", "word", "start", "end", "combinedToken"]
7
- }