@dxos/assistant 0.8.4-staging.60fe92afc8 → 0.9.1-main.c7dcc2e112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/neutral/extraction/index.mjs +108 -119
- package/dist/lib/neutral/extraction/index.mjs.map +4 -4
- package/dist/lib/neutral/index.mjs +17 -13
- package/dist/lib/neutral/index.mjs.map +4 -4
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/extraction/extraction-llm-function/index.d.ts +2 -0
- package/dist/types/src/extraction/extraction-llm-function/index.d.ts.map +1 -0
- package/dist/types/src/extraction/extraction-llm-function/proper-noun-extraction.d.ts +227 -0
- package/dist/types/src/extraction/extraction-llm-function/proper-noun-extraction.d.ts.map +1 -0
- package/dist/types/src/extraction/index.d.ts +1 -1
- package/dist/types/src/extraction/index.d.ts.map +1 -1
- package/dist/types/src/extraction/quotes.d.ts +8 -1
- package/dist/types/src/extraction/quotes.d.ts.map +1 -1
- package/dist/types/src/request/AiRequest.d.ts +6 -0
- package/dist/types/src/request/AiRequest.d.ts.map +1 -1
- package/dist/types/src/session/AiSession.d.ts +6 -0
- package/dist/types/src/session/AiSession.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -21
- package/src/extraction/extraction-llm-function/index.ts +5 -0
- package/src/extraction/extraction-llm-function/proper-noun-extraction.ts +78 -0
- package/src/extraction/index.ts +1 -1
- package/src/extraction/quotes.test.ts +67 -2
- package/src/extraction/quotes.ts +47 -13
- package/src/request/AiRequest.ts +10 -0
- package/src/session/AiSession.ts +8 -0
- package/dist/types/src/extraction/extraction.d.ts +0 -1677
- package/dist/types/src/extraction/extraction.d.ts.map +0 -1
- package/src/extraction/extraction-llm-function/transcription-instructions.tpl +0 -28
- package/src/extraction/extraction.ts +0 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1-main.c7dcc2e112",
|
|
4
4
|
"description": "Natural language interface to DXOS",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -38,26 +38,26 @@
|
|
|
38
38
|
"@effect/platform": "0.96.1",
|
|
39
39
|
"@xenova/transformers": "^2.17.2",
|
|
40
40
|
"lodash.defaultsdeep": "^4.6.1",
|
|
41
|
-
"@dxos/ai": "0.
|
|
42
|
-
"@dxos/async": "0.
|
|
43
|
-
"@dxos/compute": "0.
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/echo": "0.
|
|
47
|
-
"@dxos/echo-client": "0.
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/echo-protocol": "0.
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/log": "0.
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/node-std": "0.
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/
|
|
41
|
+
"@dxos/ai": "0.9.1-main.c7dcc2e112",
|
|
42
|
+
"@dxos/async": "0.9.1-main.c7dcc2e112",
|
|
43
|
+
"@dxos/compute": "0.9.1-main.c7dcc2e112",
|
|
44
|
+
"@dxos/context": "0.9.1-main.c7dcc2e112",
|
|
45
|
+
"@dxos/debug": "0.9.1-main.c7dcc2e112",
|
|
46
|
+
"@dxos/echo": "0.9.1-main.c7dcc2e112",
|
|
47
|
+
"@dxos/echo-client": "0.9.1-main.c7dcc2e112",
|
|
48
|
+
"@dxos/errors": "0.9.1-main.c7dcc2e112",
|
|
49
|
+
"@dxos/echo-protocol": "0.9.1-main.c7dcc2e112",
|
|
50
|
+
"@dxos/graph": "0.9.1-main.c7dcc2e112",
|
|
51
|
+
"@dxos/keys": "0.9.1-main.c7dcc2e112",
|
|
52
|
+
"@dxos/functions": "0.9.1-main.c7dcc2e112",
|
|
53
|
+
"@dxos/log": "0.9.1-main.c7dcc2e112",
|
|
54
|
+
"@dxos/mcp-client": "0.9.1-main.c7dcc2e112",
|
|
55
|
+
"@dxos/invariant": "0.9.1-main.c7dcc2e112",
|
|
56
|
+
"@dxos/node-std": "0.9.1-main.c7dcc2e112",
|
|
57
|
+
"@dxos/effect": "0.9.1-main.c7dcc2e112",
|
|
58
|
+
"@dxos/types": "0.9.1-main.c7dcc2e112",
|
|
59
|
+
"@dxos/schema": "0.9.1-main.c7dcc2e112",
|
|
60
|
+
"@dxos/util": "0.9.1-main.c7dcc2e112"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/lodash.defaultsdeep": "^4.6.6",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as LanguageModel from '@effect/ai/LanguageModel';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
|
|
9
|
+
import { AiService } from '@dxos/ai';
|
|
10
|
+
import { type Database, Obj } from '@dxos/echo';
|
|
11
|
+
import { Message } from '@dxos/types';
|
|
12
|
+
|
|
13
|
+
import { findReferences, insertReferences } from '../quotes';
|
|
14
|
+
|
|
15
|
+
const EXTRACTION_MODEL = 'ai.claude.model.claude-haiku-4-5';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Proper nouns extracted from transcript text.
|
|
19
|
+
*/
|
|
20
|
+
export const ProperNouns = Schema.Struct({
|
|
21
|
+
properNouns: Schema.Array(Schema.String).annotations({
|
|
22
|
+
description: 'Proper nouns (names of people, organizations, places, products) mentioned in the text.',
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
export type ProperNouns = Schema.Schema.Type<typeof ProperNouns>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extracts proper nouns from transcript text using a small LLM (Haiku). Provides the LanguageModel
|
|
29
|
+
* internally so the residual requirement is just {@link AiService.AiService}.
|
|
30
|
+
*/
|
|
31
|
+
export const extractProperNouns = (text: string) =>
|
|
32
|
+
Effect.gen(function* () {
|
|
33
|
+
const { value } = yield* Effect.scoped(
|
|
34
|
+
LanguageModel.generateObject({
|
|
35
|
+
schema: ProperNouns,
|
|
36
|
+
prompt: [
|
|
37
|
+
'Extract every proper noun (people, organizations, places, products) mentioned in the transcript text below.',
|
|
38
|
+
'Return only the surface strings exactly as they appear. Exclude common nouns and pronouns.',
|
|
39
|
+
'',
|
|
40
|
+
'Transcript:',
|
|
41
|
+
text,
|
|
42
|
+
].join('\n'),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Drop short tokens (e.g. "IT"): insertReferences replaces case-insensitive substrings, so a
|
|
47
|
+
// short noun would match inside unrelated words ("secur[IT]y") and corrupt neighbouring links.
|
|
48
|
+
return value.properNouns.map((noun) => noun.trim()).filter((noun) => noun.length >= 3);
|
|
49
|
+
}).pipe(Effect.provide(AiService.model(EXTRACTION_MODEL)));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Enriches a transcript message: for each transcript block, extracts proper nouns, links them to
|
|
53
|
+
* objects in the space via full-text search, and rewrites the text with `[noun](dxn)` references.
|
|
54
|
+
* Requires {@link AiService.AiService}; takes the space database handle.
|
|
55
|
+
*/
|
|
56
|
+
export const enrichTranscriptMessage = (message: Message.Message, { db }: { db: Database.Database }) =>
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
const blocks = yield* Effect.forEach(message.blocks, (block) =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
if (block._tag !== 'transcript') {
|
|
61
|
+
return block;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nouns = yield* extractProperNouns(block.text);
|
|
65
|
+
const references = yield* Effect.promise(() => findReferences([...nouns], db));
|
|
66
|
+
return { ...block, text: insertReferences(block.text, references) };
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Preserve the message identity and metadata; enrichment only rewrites transcript block text.
|
|
71
|
+
return Obj.make(Message.Message, {
|
|
72
|
+
id: message.id,
|
|
73
|
+
sender: message.sender,
|
|
74
|
+
created: message.created,
|
|
75
|
+
properties: message.properties,
|
|
76
|
+
blocks,
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/extraction/index.ts
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { describe, expect, test } from 'vitest';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Obj } from '@dxos/echo';
|
|
8
|
+
import { EchoTestBuilder } from '@dxos/echo-client/testing';
|
|
9
|
+
import { Organization, Person } from '@dxos/types';
|
|
10
|
+
|
|
11
|
+
import { findReferences, insertReferences } from './quotes';
|
|
8
12
|
|
|
9
13
|
describe('insertReferences', () => {
|
|
10
14
|
test('should replace quotes with DXN references', () => {
|
|
@@ -24,4 +28,65 @@ describe('insertReferences', () => {
|
|
|
24
28
|
"And what I'd like to talk today about is Steven Wolfram's concept of a [computational irreducibility](echo:/01JTG9JW11XGWJZ32AW8ET93D1).",
|
|
25
29
|
);
|
|
26
30
|
});
|
|
31
|
+
|
|
32
|
+
test('does not nest links when one quote is a substring of another', () => {
|
|
33
|
+
const quotes = {
|
|
34
|
+
references: [
|
|
35
|
+
{ quote: 'Sarah', id: '01JTG9JW11XGWJZ32AW8ET93D1' },
|
|
36
|
+
{ quote: 'Sarah Johnson', id: '01JTG9JW11XGWJZ32AW8ET93D1' },
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// "Sarah Johnson" is linked as a whole (processed first); the standalone "Sarah" is linked
|
|
41
|
+
// separately; the "Sarah" inside the first link is left untouched (no nested `[[..](..)](..)`).
|
|
42
|
+
expect(insertReferences('Sarah Johnson spoke. Later Sarah left.', quotes)).toBe(
|
|
43
|
+
'[Sarah Johnson](echo:/01JTG9JW11XGWJZ32AW8ET93D1) spoke. Later [Sarah](echo:/01JTG9JW11XGWJZ32AW8ET93D1) left.',
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('does not re-link a repeated quote inside an already-inserted link', () => {
|
|
48
|
+
const quotes = { references: [{ quote: 'Amco', id: '01JTG9JW11XGWJZ32AW8ET93D1' }] };
|
|
49
|
+
|
|
50
|
+
expect(insertReferences('Amco and Amco again.', quotes)).toBe(
|
|
51
|
+
'[Amco](echo:/01JTG9JW11XGWJZ32AW8ET93D1) and [Amco](echo:/01JTG9JW11XGWJZ32AW8ET93D1) again.',
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('findReferences', () => {
|
|
57
|
+
let builder: EchoTestBuilder;
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
builder = await new EchoTestBuilder().open();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
await builder.close();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('links nouns to objects via full-text search, including first-name tokens', async () => {
|
|
68
|
+
const { db } = await builder.createDatabase({ types: [Organization.Organization, Person.Person] });
|
|
69
|
+
const amco = db.add(Obj.make(Organization.Organization, { name: 'Amco' }));
|
|
70
|
+
const sarah = db.add(Obj.make(Person.Person, { fullName: 'Sarah Johnson' }));
|
|
71
|
+
await db.flush({ indexes: true });
|
|
72
|
+
|
|
73
|
+
const { references } = await findReferences(['Amco', 'Sarah', 'Nonexistent'], db);
|
|
74
|
+
const byQuote = new Map(references.map((reference) => [reference.quote, reference.id]));
|
|
75
|
+
|
|
76
|
+
expect(byQuote.get('Amco')).toEqual(amco.id);
|
|
77
|
+
// Full-text matches a single token within a multi-word field, so first-name mentions link to
|
|
78
|
+
// the full-name object — this is also what makes the overlap case in `insertReferences` real.
|
|
79
|
+
expect(byQuote.get('Sarah')).toEqual(sarah.id);
|
|
80
|
+
expect(byQuote.has('Nonexistent')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('deduplicates and trims nouns before lookup', async () => {
|
|
84
|
+
const { db } = await builder.createDatabase({ types: [Organization.Organization] });
|
|
85
|
+
db.add(Obj.make(Organization.Organization, { name: 'Amco' }));
|
|
86
|
+
await db.flush({ indexes: true });
|
|
87
|
+
|
|
88
|
+
const { references } = await findReferences(['Amco', 'Amco', ' Amco ', ''], db);
|
|
89
|
+
|
|
90
|
+
expect(references.filter((reference) => reference.quote === 'Amco')).toHaveLength(1);
|
|
91
|
+
});
|
|
27
92
|
});
|
package/src/extraction/quotes.ts
CHANGED
|
@@ -6,7 +6,6 @@ import * as Schema from 'effect/Schema';
|
|
|
6
6
|
|
|
7
7
|
import { type Database, Filter, Query } from '@dxos/echo';
|
|
8
8
|
import { EID, EntityId } from '@dxos/keys';
|
|
9
|
-
import { log } from '@dxos/log';
|
|
10
9
|
|
|
11
10
|
export const ReferencedQuotes = Schema.Struct({
|
|
12
11
|
references: Schema.Array(
|
|
@@ -26,12 +25,30 @@ export const ReferencedQuotes = Schema.Struct({
|
|
|
26
25
|
});
|
|
27
26
|
export type ReferencedQuotes = Schema.Schema.Type<typeof ReferencedQuotes>;
|
|
28
27
|
|
|
29
|
-
export
|
|
28
|
+
export type FindReferencesOptions = {
|
|
29
|
+
/** Search backend. Full-text matches proper nouns precisely; vector matches semantically. */
|
|
30
|
+
searchKind?: 'full-text' | 'vector';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Finds the best-matching context object for each noun and returns it as a reference.
|
|
35
|
+
*/
|
|
36
|
+
export const findReferences = async (
|
|
37
|
+
nouns: string[],
|
|
38
|
+
db: Database.Database,
|
|
39
|
+
{ searchKind = 'full-text' }: FindReferencesOptions = {},
|
|
40
|
+
): Promise<ReferencedQuotes> => {
|
|
41
|
+
// Deduplicate and process longer nouns first: `insertReferences` rewrites case-insensitive
|
|
42
|
+
// substrings, so a repeated or shorter overlapping noun would re-replace already-linked text and
|
|
43
|
+
// produce nested/broken markdown links.
|
|
44
|
+
const normalizedNouns = Array.from(new Set(nouns.map((noun) => noun.trim()).filter((noun) => noun.length > 0))).sort(
|
|
45
|
+
(leftNoun, rightNoun) => rightNoun.length - leftNoun.length,
|
|
46
|
+
);
|
|
47
|
+
|
|
30
48
|
const references = await Promise.all(
|
|
31
|
-
|
|
32
|
-
const objects = await db.query(Query.select(Filter.text(
|
|
33
|
-
|
|
34
|
-
return objects.length > 0 ? [{ id: objects[0].id.toString(), quote }] : [];
|
|
49
|
+
normalizedNouns.map(async (noun) => {
|
|
50
|
+
const objects = await db.query(Query.select(Filter.text(noun, { type: searchKind }))).run();
|
|
51
|
+
return objects.length > 0 ? [{ id: objects[0].id.toString(), quote: noun }] : [];
|
|
35
52
|
}),
|
|
36
53
|
);
|
|
37
54
|
return { references: references.flat() };
|
|
@@ -41,16 +58,33 @@ export const findQuotes = async (quotes: string[], db: Database.Database): Promi
|
|
|
41
58
|
* Finds and replaces all quotes with DXNs references.
|
|
42
59
|
*/
|
|
43
60
|
// TODO(dmaretskyi): Lookup and verifiy ids from provided context.
|
|
44
|
-
export const insertReferences = (text: string, quotes: ReferencedQuotes) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
export const insertReferences = (text: string, quotes: ReferencedQuotes): string => {
|
|
62
|
+
// Process longer quotes first so e.g. "Sarah Johnson" is linked before "Sarah". Combined with the
|
|
63
|
+
// link-aware replacement below this stops a shorter or overlapping quote from rewriting text that
|
|
64
|
+
// is already part of an inserted link (which would produce nested `[[..](..)](..)` markdown).
|
|
65
|
+
const references = quotes.references
|
|
66
|
+
.filter((quote) => EntityId.isValid(quote.id))
|
|
67
|
+
.sort((left, right) => right.quote.length - left.quote.length);
|
|
49
68
|
|
|
50
|
-
|
|
69
|
+
for (const quote of references) {
|
|
70
|
+
// Case-insensitive match of the (regex-escaped) quote.
|
|
51
71
|
const regex = new RegExp(quote.quote.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
52
|
-
text = text
|
|
72
|
+
text = replaceOutsideLinks(text, regex, `[${quote.quote}](${EID.make({ entityId: quote.id })})`);
|
|
53
73
|
}
|
|
54
74
|
|
|
55
75
|
return text;
|
|
56
76
|
};
|
|
77
|
+
|
|
78
|
+
const MARKDOWN_LINK = /\[[^\]]*\]\([^)]*\)/g;
|
|
79
|
+
|
|
80
|
+
/** Applies a replacement to every span of `text` that is not already inside a `[label](url)` link. */
|
|
81
|
+
const replaceOutsideLinks = (text: string, regex: RegExp, replacement: string): string => {
|
|
82
|
+
let result = '';
|
|
83
|
+
let plainStart = 0;
|
|
84
|
+
for (const link of text.matchAll(MARKDOWN_LINK)) {
|
|
85
|
+
const index = link.index ?? 0;
|
|
86
|
+
result += text.slice(plainStart, index).replace(regex, replacement) + link[0];
|
|
87
|
+
plainStart = index + link[0].length;
|
|
88
|
+
}
|
|
89
|
+
return result + text.slice(plainStart).replace(regex, replacement);
|
|
90
|
+
};
|
package/src/request/AiRequest.ts
CHANGED
|
@@ -60,6 +60,13 @@ export type Options = {
|
|
|
60
60
|
* This is useful for streaming the output to a queue.
|
|
61
61
|
*/
|
|
62
62
|
onOutput?: (message: Message.Message) => Effect.Effect<void, never, never>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* When false, turn messages are not appended to the feed or written as persisted trace blocks.
|
|
66
|
+
*
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
persist?: boolean;
|
|
63
70
|
};
|
|
64
71
|
|
|
65
72
|
export type RunProps<R = never> = {
|
|
@@ -139,6 +146,9 @@ export class Request {
|
|
|
139
146
|
Effect.gen(this, function* () {
|
|
140
147
|
this._pending.push(message);
|
|
141
148
|
yield* this._observer.onMessage(message);
|
|
149
|
+
if (this._options.persist === false) {
|
|
150
|
+
return message;
|
|
151
|
+
}
|
|
142
152
|
for (const block of message.blocks) {
|
|
143
153
|
log('write complete block', {
|
|
144
154
|
messageId: message.id,
|
package/src/session/AiSession.ts
CHANGED
|
@@ -41,6 +41,13 @@ export type RunProps<R = never> = {
|
|
|
41
41
|
* Space-level MCP servers to connect alongside blueprint-defined ones.
|
|
42
42
|
*/
|
|
43
43
|
mcpServers?: readonly McpServer.McpServer[];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* When false, messages from this request are not appended to the feed or persisted to trace.
|
|
47
|
+
*
|
|
48
|
+
* @default true
|
|
49
|
+
*/
|
|
50
|
+
persist?: boolean;
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
export type Options = {
|
|
@@ -145,6 +152,7 @@ export class Session extends Resource {
|
|
|
145
152
|
const request = new AiRequest.Request({
|
|
146
153
|
summarizationThreshold: SUMMARY_THRESHOLD,
|
|
147
154
|
observer: params.observer,
|
|
155
|
+
persist: params.persist,
|
|
148
156
|
onOutput: (message) =>
|
|
149
157
|
Effect.promise(() => Runtime.runPromise(this._runtime)(Feed.append(this._feed, [message]))),
|
|
150
158
|
});
|