@dxos/assistant 0.9.0 → 0.9.1-staging.ee54ba693a
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 +395 -234
- 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 +26 -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/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/request/AiRequest.d.ts +19 -208
- package/dist/types/src/request/AiRequest.d.ts.map +1 -1
- package/dist/types/src/request/format.d.ts +1 -1
- package/dist/types/src/request/format.d.ts.map +1 -1
- package/dist/types/src/session/AiContext.d.ts +27 -105
- package/dist/types/src/session/AiContext.d.ts.map +1 -1
- package/dist/types/src/session/AiSession.d.ts +11 -29
- package/dist/types/src/session/AiSession.d.ts.map +1 -1
- package/dist/types/src/session/Harness.d.ts +122 -0
- package/dist/types/src/session/Harness.d.ts.map +1 -0
- package/dist/types/src/session/SessionLink.d.ts +8 -11
- package/dist/types/src/session/SessionLink.d.ts.map +1 -1
- package/dist/types/src/session/SessionLoader.d.ts +2 -2
- package/dist/types/src/session/SessionLoader.d.ts.map +1 -1
- package/dist/types/src/session/SkillHooks.d.ts +28 -0
- package/dist/types/src/session/SkillHooks.d.ts.map +1 -0
- package/dist/types/src/session/harness-control.d.ts +177 -0
- package/dist/types/src/session/harness-control.d.ts.map +1 -0
- package/dist/types/src/session/index.d.ts +3 -0
- package/dist/types/src/session/index.d.ts.map +1 -1
- package/dist/types/src/session/toolkit.d.ts +4 -4
- package/dist/types/src/session/toolkit.d.ts.map +1 -1
- package/dist/types/src/templates/system.d.ts +4 -6
- package/dist/types/src/templates/system.d.ts.map +1 -1
- package/dist/types/src/tool-runtime/index.d.ts.map +1 -0
- package/dist/types/src/tool-runtime/services.d.ts.map +1 -0
- package/dist/types/src/util/artifact.d.ts.map +1 -1
- package/dist/types/src/util/diff.d.ts +9 -3
- package/dist/types/src/util/diff.d.ts.map +1 -1
- package/dist/types/src/util/tracing.d.ts +40 -0
- package/dist/types/src/util/tracing.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -25
- 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/index.ts +1 -1
- package/src/request/AiRequest.ts +45 -14
- package/src/request/format.ts +9 -11
- package/src/session/AiContext.test.ts +52 -32
- package/src/session/AiContext.ts +73 -95
- package/src/session/AiSession.test.ts +4 -4
- package/src/session/AiSession.ts +51 -83
- package/src/session/Harness.ts +241 -0
- package/src/session/SessionLink.ts +8 -8
- package/src/session/SessionLoader.ts +4 -4
- package/src/session/SkillHooks.ts +71 -0
- package/src/session/harness-control.ts +30 -0
- package/src/session/index.ts +3 -0
- package/src/session/toolkit.ts +8 -8
- package/src/templates/instructions/format.tpl +3 -3
- package/src/templates/instructions/system.tpl +10 -12
- package/src/templates/system.ts +6 -2
- package/src/util/artifact.ts +9 -2
- package/src/util/diff.test.ts +4 -4
- package/src/util/diff.ts +15 -8
- package/src/util/tracing.ts +1 -1
- package/dist/types/src/extraction/extraction.d.ts +0 -1677
- package/dist/types/src/extraction/extraction.d.ts.map +0 -1
- package/dist/types/src/functions/index.d.ts.map +0 -1
- package/dist/types/src/functions/services.d.ts.map +0 -1
- package/src/extraction/extraction-llm-function/transcription-instructions.tpl +0 -28
- package/src/extraction/extraction.ts +0 -105
- /package/dist/types/src/{functions → tool-runtime}/index.d.ts +0 -0
- /package/dist/types/src/{functions → tool-runtime}/services.d.ts +0 -0
- /package/src/{functions → tool-runtime}/index.ts +0 -0
- /package/src/{functions → tool-runtime}/services.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/assistant",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-staging.ee54ba693a",
|
|
4
4
|
"description": "Natural language interface to DXOS",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -30,41 +30,44 @@
|
|
|
30
30
|
"src"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@automerge/automerge": "3.3.0-fragments.
|
|
33
|
+
"@automerge/automerge": "3.3.0-fragments.2",
|
|
34
34
|
"@effect-atom/atom": "^0.5.3",
|
|
35
35
|
"@effect-atom/atom-react": "^0.5.0",
|
|
36
36
|
"@effect/ai": "0.36.0",
|
|
37
37
|
"@effect/experimental": "0.60.0",
|
|
38
|
-
"@effect/platform": "0.96.
|
|
38
|
+
"@effect/platform": "0.96.2",
|
|
39
|
+
"@effect/rpc": "0.75.1",
|
|
39
40
|
"@xenova/transformers": "^2.17.2",
|
|
40
41
|
"lodash.defaultsdeep": "^4.6.1",
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/echo-
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/
|
|
42
|
+
"@dxos/ai": "0.9.1-staging.ee54ba693a",
|
|
43
|
+
"@dxos/async": "0.9.1-staging.ee54ba693a",
|
|
44
|
+
"@dxos/compute": "0.9.1-staging.ee54ba693a",
|
|
45
|
+
"@dxos/compute-runtime": "0.9.1-staging.ee54ba693a",
|
|
46
|
+
"@dxos/debug": "0.9.1-staging.ee54ba693a",
|
|
47
|
+
"@dxos/context": "0.9.1-staging.ee54ba693a",
|
|
48
|
+
"@dxos/echo-client": "0.9.1-staging.ee54ba693a",
|
|
49
|
+
"@dxos/echo-doc": "0.9.1-staging.ee54ba693a",
|
|
50
|
+
"@dxos/effect": "0.9.1-staging.ee54ba693a",
|
|
51
|
+
"@dxos/echo": "0.9.1-staging.ee54ba693a",
|
|
52
|
+
"@dxos/echo-protocol": "0.9.1-staging.ee54ba693a",
|
|
53
|
+
"@dxos/errors": "0.9.1-staging.ee54ba693a",
|
|
54
|
+
"@dxos/functions": "0.9.1-staging.ee54ba693a",
|
|
55
|
+
"@dxos/graph": "0.9.1-staging.ee54ba693a",
|
|
56
|
+
"@dxos/keys": "0.9.1-staging.ee54ba693a",
|
|
57
|
+
"@dxos/log": "0.9.1-staging.ee54ba693a",
|
|
58
|
+
"@dxos/invariant": "0.9.1-staging.ee54ba693a",
|
|
59
|
+
"@dxos/mcp-client": "0.9.1-staging.ee54ba693a",
|
|
60
|
+
"@dxos/node-std": "0.9.1-staging.ee54ba693a",
|
|
61
|
+
"@dxos/schema": "0.9.1-staging.ee54ba693a",
|
|
62
|
+
"@dxos/types": "0.9.1-staging.ee54ba693a",
|
|
63
|
+
"@dxos/util": "0.9.1-staging.ee54ba693a"
|
|
61
64
|
},
|
|
62
65
|
"devDependencies": {
|
|
63
66
|
"@types/lodash.defaultsdeep": "^4.6.6",
|
|
64
|
-
"effect": "3.21.
|
|
67
|
+
"effect": "3.21.4"
|
|
65
68
|
},
|
|
66
69
|
"peerDependencies": {
|
|
67
|
-
"effect": "3.21.
|
|
70
|
+
"effect": "3.21.4"
|
|
68
71
|
},
|
|
69
72
|
"publishConfig": {
|
|
70
73
|
"access": "public"
|
|
@@ -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 = 'com.anthropic.model.claude-haiku-4-5.default';
|
|
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/index.ts
CHANGED
package/src/request/AiRequest.ts
CHANGED
|
@@ -19,19 +19,19 @@ import {
|
|
|
19
19
|
AiPreprocessor,
|
|
20
20
|
AiSummarizer,
|
|
21
21
|
type AiToolNotFoundError,
|
|
22
|
-
callTool,
|
|
23
22
|
type OpaqueToolkit,
|
|
24
23
|
type PromptPreprocessingError,
|
|
25
24
|
type ToolExecutionService,
|
|
26
25
|
type ToolResolverService,
|
|
26
|
+
callTool,
|
|
27
27
|
withoutToolCallParising,
|
|
28
28
|
} from '@dxos/ai';
|
|
29
|
-
import { type
|
|
29
|
+
import { Operation, type Skill, Trace } from '@dxos/compute';
|
|
30
30
|
import { Database, Obj, Registry } from '@dxos/echo';
|
|
31
31
|
import { log } from '@dxos/log';
|
|
32
32
|
import { ContentBlock, Message } from '@dxos/types';
|
|
33
33
|
|
|
34
|
-
import { getOperationFromTool } from '../
|
|
34
|
+
import { getOperationFromTool } from '../tool-runtime/services';
|
|
35
35
|
import { type AiAssistantError, CompleteBlock, PartialBlock } from '../util';
|
|
36
36
|
import { formatSystemPrompt, formatUserPrompt } from './format';
|
|
37
37
|
import { GenerationObserver } from './observer';
|
|
@@ -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> = {
|
|
@@ -68,7 +75,7 @@ export type RunProps<R = never> = {
|
|
|
68
75
|
system?: string;
|
|
69
76
|
history?: Message.Message[];
|
|
70
77
|
objects?: Obj.Unknown[];
|
|
71
|
-
|
|
78
|
+
skills?: readonly Skill.Skill[];
|
|
72
79
|
toolkit?: OpaqueToolkit.OpaqueToolkit<R>;
|
|
73
80
|
};
|
|
74
81
|
|
|
@@ -77,7 +84,7 @@ export type BeginProps = {
|
|
|
77
84
|
system?: string;
|
|
78
85
|
history?: Message.Message[];
|
|
79
86
|
objects?: Obj.Unknown[];
|
|
80
|
-
|
|
87
|
+
skills?: readonly Skill.Skill[];
|
|
81
88
|
};
|
|
82
89
|
|
|
83
90
|
export type TurnProps<R = never> = {
|
|
@@ -88,6 +95,12 @@ export type TurnProps<R = never> = {
|
|
|
88
95
|
export type TurnResult = {
|
|
89
96
|
messages: Message.Message[];
|
|
90
97
|
done: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Provider finish reason for this turn. `pause` means the provider paused a server-tool turn
|
|
100
|
+
* mid-execution (e.g. Anthropic `pause_turn`) and the turn must be resumed by issuing another
|
|
101
|
+
* request without mutating the trailing assistant content.
|
|
102
|
+
*/
|
|
103
|
+
finishReason?: ContentBlock.FinishReason;
|
|
91
104
|
};
|
|
92
105
|
|
|
93
106
|
/**
|
|
@@ -139,6 +152,9 @@ export class Request {
|
|
|
139
152
|
Effect.gen(this, function* () {
|
|
140
153
|
this._pending.push(message);
|
|
141
154
|
yield* this._observer.onMessage(message);
|
|
155
|
+
if (this._options.persist === false) {
|
|
156
|
+
return message;
|
|
157
|
+
}
|
|
142
158
|
for (const block of message.blocks) {
|
|
143
159
|
log('write complete block', {
|
|
144
160
|
messageId: message.id,
|
|
@@ -173,7 +189,7 @@ export class Request {
|
|
|
173
189
|
prompt,
|
|
174
190
|
system,
|
|
175
191
|
history = [],
|
|
176
|
-
|
|
192
|
+
skills = [],
|
|
177
193
|
objects = [],
|
|
178
194
|
}: BeginProps): Effect.Effect<void, RunError, RunRequirements> =>
|
|
179
195
|
Effect.gen(this, function* () {
|
|
@@ -181,7 +197,7 @@ export class Request {
|
|
|
181
197
|
this._history = [...history];
|
|
182
198
|
this._pending = [];
|
|
183
199
|
|
|
184
|
-
const systemPrompt = yield* formatSystemPrompt({ system,
|
|
200
|
+
const systemPrompt = yield* formatSystemPrompt({ system, skills, objects }).pipe(Effect.orDie);
|
|
185
201
|
|
|
186
202
|
if (this._options.summarizationThreshold !== undefined) {
|
|
187
203
|
const tokenCount = yield* AiPreprocessor.estimateTokens(
|
|
@@ -200,7 +216,7 @@ export class Request {
|
|
|
200
216
|
|
|
201
217
|
/**
|
|
202
218
|
* Execute a single turn: one LLM generation followed by tool execution.
|
|
203
|
-
* The toolkit and system prompt can be updated between turns to reflect context changes (e.g. dynamically enabled
|
|
219
|
+
* The toolkit and system prompt can be updated between turns to reflect context changes (e.g. dynamically enabled skills).
|
|
204
220
|
*/
|
|
205
221
|
runAgentTurn = <const R = never>({
|
|
206
222
|
system,
|
|
@@ -222,6 +238,7 @@ export class Request {
|
|
|
222
238
|
|
|
223
239
|
const observer = this._observer;
|
|
224
240
|
let currentMessageId: Obj.ID | null = null;
|
|
241
|
+
let finishReason: ContentBlock.FinishReason | undefined;
|
|
225
242
|
|
|
226
243
|
const messages = yield* LanguageModel.streamText({
|
|
227
244
|
prompt,
|
|
@@ -240,6 +257,9 @@ export class Request {
|
|
|
240
257
|
Stream.mapEffect(
|
|
241
258
|
(block) =>
|
|
242
259
|
Effect.gen(this, function* () {
|
|
260
|
+
if (block._tag === 'stats' && block.finishReason !== undefined) {
|
|
261
|
+
finishReason = block.finishReason;
|
|
262
|
+
}
|
|
243
263
|
if (block.pending) {
|
|
244
264
|
currentMessageId ??= Obj.ID.random();
|
|
245
265
|
log('emit ephemeral message', { id: currentMessageId, type: block._tag });
|
|
@@ -271,16 +291,23 @@ export class Request {
|
|
|
271
291
|
);
|
|
272
292
|
log('messages', { messages });
|
|
273
293
|
|
|
294
|
+
// A paused server-tool turn (e.g. Anthropic `pause_turn`) is not complete: the provider
|
|
295
|
+
// expects the assistant content to be resent so it can finish executing the server tool.
|
|
296
|
+
// No local tool execution is needed — just another request.
|
|
297
|
+
if (finishReason === 'pause') {
|
|
298
|
+
return { messages, done: false, finishReason };
|
|
299
|
+
}
|
|
300
|
+
|
|
274
301
|
const toolCalls = this.getToolCalls();
|
|
275
302
|
|
|
276
303
|
if (toolCalls.length === 0) {
|
|
277
304
|
this._ended = Date.now();
|
|
278
|
-
return { messages, done: true };
|
|
305
|
+
return { messages, done: true, finishReason };
|
|
279
306
|
} else if (!toolkit) {
|
|
280
307
|
throw new Error('No toolkit provided');
|
|
281
308
|
}
|
|
282
309
|
|
|
283
|
-
return { messages, done: false };
|
|
310
|
+
return { messages, done: false, finishReason };
|
|
284
311
|
}).pipe(Effect.withSpan('AiRequest.runAgentTurn'));
|
|
285
312
|
|
|
286
313
|
runTools = <const R = never>({
|
|
@@ -317,19 +344,23 @@ export class Request {
|
|
|
317
344
|
system: systemTemplate,
|
|
318
345
|
history = [],
|
|
319
346
|
objects = [],
|
|
320
|
-
|
|
347
|
+
skills = [],
|
|
321
348
|
toolkit,
|
|
322
349
|
}: RunProps<R>): Effect.Effect<Message.Message[], RunError, RunRequirements | R> =>
|
|
323
350
|
Effect.gen(this, function* () {
|
|
324
|
-
yield* this.begin({ prompt, system: systemTemplate, history, objects,
|
|
351
|
+
yield* this.begin({ prompt, system: systemTemplate, history, objects, skills });
|
|
325
352
|
|
|
326
|
-
const system = yield* formatSystemPrompt({ system: systemTemplate,
|
|
353
|
+
const system = yield* formatSystemPrompt({ system: systemTemplate, skills, objects }).pipe(Effect.orDie);
|
|
327
354
|
|
|
328
355
|
do {
|
|
329
|
-
const { done } = yield* this.runAgentTurn({ system, toolkit });
|
|
356
|
+
const { done, finishReason } = yield* this.runAgentTurn({ system, toolkit });
|
|
330
357
|
if (done) {
|
|
331
358
|
break;
|
|
332
359
|
}
|
|
360
|
+
// A paused server-tool turn resumes with another request and no local tool execution.
|
|
361
|
+
if (finishReason === 'pause') {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
333
364
|
yield* this.runTools({ toolkit });
|
|
334
365
|
} while (true);
|
|
335
366
|
|
package/src/request/format.ts
CHANGED
|
@@ -25,31 +25,29 @@ import { ArtifactDiffResolver } from './artifact-diff';
|
|
|
25
25
|
// TODO(burdon): Move to AiPreprocessor.
|
|
26
26
|
export const formatSystemPrompt = ({
|
|
27
27
|
system,
|
|
28
|
-
|
|
28
|
+
skills = [],
|
|
29
29
|
objects = [],
|
|
30
|
-
}: Pick<AiRequest.RunProps, 'system' | '
|
|
30
|
+
}: Pick<AiRequest.RunProps, 'system' | 'skills' | 'objects'>): Effect.Effect<
|
|
31
31
|
string,
|
|
32
32
|
FunctionNotFoundError | EntityNotFoundError,
|
|
33
33
|
Database.Service | Registry.Service | Operation.Service
|
|
34
34
|
> =>
|
|
35
35
|
Effect.gen(function* () {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
Effect.forEach((
|
|
36
|
+
const skillDefs = yield* Function.pipe(
|
|
37
|
+
skills,
|
|
38
|
+
Effect.forEach((skill) => Effect.succeed(skill.instructions)),
|
|
39
39
|
Effect.flatMap(
|
|
40
40
|
Effect.forEach((template) =>
|
|
41
41
|
Effect.gen(function* () {
|
|
42
42
|
return trim`
|
|
43
|
-
<
|
|
43
|
+
<skill>
|
|
44
44
|
${yield* Template.processTemplate(template)}
|
|
45
|
-
</
|
|
45
|
+
</skill>
|
|
46
46
|
`;
|
|
47
47
|
}),
|
|
48
48
|
),
|
|
49
49
|
),
|
|
50
|
-
Effect.map((
|
|
51
|
-
blueprints.length > 0 ? ['## Blueprints Definitions', ...blueprints].join('\n\n') : undefined,
|
|
52
|
-
),
|
|
50
|
+
Effect.map((skills) => (skills.length > 0 ? ['## Skills Definitions', ...skills].join('\n\n') : undefined)),
|
|
53
51
|
);
|
|
54
52
|
|
|
55
53
|
const objectDefs = yield* Function.pipe(
|
|
@@ -66,7 +64,7 @@ export const formatSystemPrompt = ({
|
|
|
66
64
|
);
|
|
67
65
|
|
|
68
66
|
return yield* Function.pipe(
|
|
69
|
-
Effect.succeed([system,
|
|
67
|
+
Effect.succeed([system, skillDefs, objectDefs].filter((def): def is string => def !== undefined)),
|
|
70
68
|
Effect.map((parts) => parts.join('\n\n')),
|
|
71
69
|
);
|
|
72
70
|
}).pipe(Effect.withSpan('formatSystemPrompt'));
|