@dxos/assistant-toolkit 0.8.4-main.ae835ea

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 (200) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/lib/browser/index.mjs +2481 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/lib/node-esm/index.mjs +2483 -0
  7. package/dist/lib/node-esm/index.mjs.map +7 -0
  8. package/dist/lib/node-esm/meta.json +1 -0
  9. package/dist/types/src/blueprints/design/design-blueprint.d.ts +4 -0
  10. package/dist/types/src/blueprints/design/design-blueprint.d.ts.map +1 -0
  11. package/dist/types/src/blueprints/design/design-blueprint.test.d.ts +2 -0
  12. package/dist/types/src/blueprints/design/design-blueprint.test.d.ts.map +1 -0
  13. package/dist/types/src/blueprints/design/index.d.ts +3 -0
  14. package/dist/types/src/blueprints/design/index.d.ts.map +1 -0
  15. package/dist/types/src/blueprints/discord/discord-blueprint.d.ts +18 -0
  16. package/dist/types/src/blueprints/discord/discord-blueprint.d.ts.map +1 -0
  17. package/dist/types/src/blueprints/discord/index.d.ts +3 -0
  18. package/dist/types/src/blueprints/discord/index.d.ts.map +1 -0
  19. package/dist/types/src/blueprints/index.d.ts +7 -0
  20. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  21. package/dist/types/src/blueprints/linear/index.d.ts +3 -0
  22. package/dist/types/src/blueprints/linear/index.d.ts.map +1 -0
  23. package/dist/types/src/blueprints/linear/linear-blueprint.d.ts +18 -0
  24. package/dist/types/src/blueprints/linear/linear-blueprint.d.ts.map +1 -0
  25. package/dist/types/src/blueprints/planning/index.d.ts +3 -0
  26. package/dist/types/src/blueprints/planning/index.d.ts.map +1 -0
  27. package/dist/types/src/blueprints/planning/planning-blueprint.d.ts +4 -0
  28. package/dist/types/src/blueprints/planning/planning-blueprint.d.ts.map +1 -0
  29. package/dist/types/src/blueprints/planning/planning-blueprint.test.d.ts +2 -0
  30. package/dist/types/src/blueprints/planning/planning-blueprint.test.d.ts.map +1 -0
  31. package/dist/types/src/blueprints/research/index.d.ts +3 -0
  32. package/dist/types/src/blueprints/research/index.d.ts.map +1 -0
  33. package/dist/types/src/blueprints/research/research-blueprint.d.ts +4 -0
  34. package/dist/types/src/blueprints/research/research-blueprint.d.ts.map +1 -0
  35. package/dist/types/src/blueprints/research/research-blueprint.test.d.ts +2 -0
  36. package/dist/types/src/blueprints/research/research-blueprint.test.d.ts.map +1 -0
  37. package/dist/types/src/blueprints/testing.d.ts +12 -0
  38. package/dist/types/src/blueprints/testing.d.ts.map +1 -0
  39. package/dist/types/src/blueprints/websearch/index.d.ts +4 -0
  40. package/dist/types/src/blueprints/websearch/index.d.ts.map +1 -0
  41. package/dist/types/src/blueprints/websearch/websearch-blueprint.d.ts +4 -0
  42. package/dist/types/src/blueprints/websearch/websearch-blueprint.d.ts.map +1 -0
  43. package/dist/types/src/blueprints/websearch/websearch-toolkit.d.ts +26 -0
  44. package/dist/types/src/blueprints/websearch/websearch-toolkit.d.ts.map +1 -0
  45. package/dist/types/src/experimental/feed.test.d.ts +2 -0
  46. package/dist/types/src/experimental/feed.test.d.ts.map +1 -0
  47. package/dist/types/src/functions/agent/index.d.ts +5 -0
  48. package/dist/types/src/functions/agent/index.d.ts.map +1 -0
  49. package/dist/types/src/functions/agent/prompt.d.ts +11 -0
  50. package/dist/types/src/functions/agent/prompt.d.ts.map +1 -0
  51. package/dist/types/src/functions/discord/fetch-messages.d.ts +11 -0
  52. package/dist/types/src/functions/discord/fetch-messages.d.ts.map +1 -0
  53. package/dist/types/src/functions/discord/fetch-messages.test.d.ts +2 -0
  54. package/dist/types/src/functions/discord/fetch-messages.test.d.ts.map +1 -0
  55. package/dist/types/src/functions/discord/index.d.ts +12 -0
  56. package/dist/types/src/functions/discord/index.d.ts.map +1 -0
  57. package/dist/types/src/functions/document/index.d.ts +12 -0
  58. package/dist/types/src/functions/document/index.d.ts.map +1 -0
  59. package/dist/types/src/functions/document/read.d.ts +7 -0
  60. package/dist/types/src/functions/document/read.d.ts.map +1 -0
  61. package/dist/types/src/functions/document/update.d.ts +6 -0
  62. package/dist/types/src/functions/document/update.d.ts.map +1 -0
  63. package/dist/types/src/functions/entity-extraction/entity-extraction.d.ts +173 -0
  64. package/dist/types/src/functions/entity-extraction/entity-extraction.d.ts.map +1 -0
  65. package/dist/types/src/functions/entity-extraction/entity-extraction.test.d.ts +2 -0
  66. package/dist/types/src/functions/entity-extraction/entity-extraction.test.d.ts.map +1 -0
  67. package/dist/types/src/functions/entity-extraction/index.d.ts +174 -0
  68. package/dist/types/src/functions/entity-extraction/index.d.ts.map +1 -0
  69. package/dist/types/src/functions/exa/exa.d.ts +5 -0
  70. package/dist/types/src/functions/exa/exa.d.ts.map +1 -0
  71. package/dist/types/src/functions/exa/index.d.ts +3 -0
  72. package/dist/types/src/functions/exa/index.d.ts.map +1 -0
  73. package/dist/types/src/functions/exa/mock.d.ts +5 -0
  74. package/dist/types/src/functions/exa/mock.d.ts.map +1 -0
  75. package/dist/types/src/functions/github/fetch-prs.d.ts +6 -0
  76. package/dist/types/src/functions/github/fetch-prs.d.ts.map +1 -0
  77. package/dist/types/src/functions/index.d.ts +8 -0
  78. package/dist/types/src/functions/index.d.ts.map +1 -0
  79. package/dist/types/src/functions/linear/index.d.ts +9 -0
  80. package/dist/types/src/functions/linear/index.d.ts.map +1 -0
  81. package/dist/types/src/functions/linear/linear.test.d.ts +2 -0
  82. package/dist/types/src/functions/linear/linear.test.d.ts.map +1 -0
  83. package/dist/types/src/functions/linear/sync-issues.d.ts +12 -0
  84. package/dist/types/src/functions/linear/sync-issues.d.ts.map +1 -0
  85. package/dist/types/src/functions/research/create-document.d.ts +7 -0
  86. package/dist/types/src/functions/research/create-document.d.ts.map +1 -0
  87. package/dist/types/src/functions/research/graph.d.ts +64 -0
  88. package/dist/types/src/functions/research/graph.d.ts.map +1 -0
  89. package/dist/types/src/functions/research/graph.test.d.ts +2 -0
  90. package/dist/types/src/functions/research/graph.test.d.ts.map +1 -0
  91. package/dist/types/src/functions/research/index.d.ts +19 -0
  92. package/dist/types/src/functions/research/index.d.ts.map +1 -0
  93. package/dist/types/src/functions/research/research-graph.d.ts +18 -0
  94. package/dist/types/src/functions/research/research-graph.d.ts.map +1 -0
  95. package/dist/types/src/functions/research/research.d.ts +13 -0
  96. package/dist/types/src/functions/research/research.d.ts.map +1 -0
  97. package/dist/types/src/functions/research/research.test.d.ts +2 -0
  98. package/dist/types/src/functions/research/research.test.d.ts.map +1 -0
  99. package/dist/types/src/functions/research/types.d.ts +384 -0
  100. package/dist/types/src/functions/research/types.d.ts.map +1 -0
  101. package/dist/types/src/functions/tasks/index.d.ts +15 -0
  102. package/dist/types/src/functions/tasks/index.d.ts.map +1 -0
  103. package/dist/types/src/functions/tasks/read.d.ts +7 -0
  104. package/dist/types/src/functions/tasks/read.d.ts.map +1 -0
  105. package/dist/types/src/functions/tasks/task-list.d.ts +74 -0
  106. package/dist/types/src/functions/tasks/task-list.d.ts.map +1 -0
  107. package/dist/types/src/functions/tasks/task-list.test.d.ts +2 -0
  108. package/dist/types/src/functions/tasks/task-list.test.d.ts.map +1 -0
  109. package/dist/types/src/functions/tasks/update.d.ts +9 -0
  110. package/dist/types/src/functions/tasks/update.d.ts.map +1 -0
  111. package/dist/types/src/index.d.ts +5 -0
  112. package/dist/types/src/index.d.ts.map +1 -0
  113. package/dist/types/src/plugins.d.ts +19 -0
  114. package/dist/types/src/plugins.d.ts.map +1 -0
  115. package/dist/types/src/sync/index.d.ts +2 -0
  116. package/dist/types/src/sync/index.d.ts.map +1 -0
  117. package/dist/types/src/sync/sync.d.ts +15 -0
  118. package/dist/types/src/sync/sync.d.ts.map +1 -0
  119. package/dist/types/src/testing/data/exa-search-1748337321991.d.ts +38 -0
  120. package/dist/types/src/testing/data/exa-search-1748337321991.d.ts.map +1 -0
  121. package/dist/types/src/testing/data/exa-search-1748337331526.d.ts +37 -0
  122. package/dist/types/src/testing/data/exa-search-1748337331526.d.ts.map +1 -0
  123. package/dist/types/src/testing/data/exa-search-1748337344119.d.ts +58 -0
  124. package/dist/types/src/testing/data/exa-search-1748337344119.d.ts.map +1 -0
  125. package/dist/types/src/testing/data/index.d.ts +3 -0
  126. package/dist/types/src/testing/data/index.d.ts.map +1 -0
  127. package/dist/types/src/testing/index.d.ts +2 -0
  128. package/dist/types/src/testing/index.d.ts.map +1 -0
  129. package/dist/types/src/util/graphql.d.ts +22 -0
  130. package/dist/types/src/util/graphql.d.ts.map +1 -0
  131. package/dist/types/src/util/index.d.ts +2 -0
  132. package/dist/types/src/util/index.d.ts.map +1 -0
  133. package/dist/types/tsconfig.tsbuildinfo +1 -0
  134. package/package.json +67 -0
  135. package/src/blueprints/design/design-blueprint.test.ts +108 -0
  136. package/src/blueprints/design/design-blueprint.ts +33 -0
  137. package/src/blueprints/design/index.ts +7 -0
  138. package/src/blueprints/discord/discord-blueprint.ts +34 -0
  139. package/src/blueprints/discord/index.ts +7 -0
  140. package/src/blueprints/index.ts +10 -0
  141. package/src/blueprints/linear/index.ts +7 -0
  142. package/src/blueprints/linear/linear-blueprint.ts +35 -0
  143. package/src/blueprints/planning/index.ts +7 -0
  144. package/src/blueprints/planning/planning-blueprint.test.ts +129 -0
  145. package/src/blueprints/planning/planning-blueprint.ts +98 -0
  146. package/src/blueprints/research/index.ts +7 -0
  147. package/src/blueprints/research/research-blueprint.test.ts +7 -0
  148. package/src/blueprints/research/research-blueprint.ts +45 -0
  149. package/src/blueprints/testing.ts +34 -0
  150. package/src/blueprints/websearch/index.ts +8 -0
  151. package/src/blueprints/websearch/websearch-blueprint.ts +20 -0
  152. package/src/blueprints/websearch/websearch-toolkit.ts +8 -0
  153. package/src/experimental/feed.test.ts +108 -0
  154. package/src/functions/agent/index.ts +11 -0
  155. package/src/functions/agent/prompt.ts +101 -0
  156. package/src/functions/discord/fetch-messages.test.ts +59 -0
  157. package/src/functions/discord/fetch-messages.ts +251 -0
  158. package/src/functions/discord/index.ts +9 -0
  159. package/src/functions/document/index.ts +11 -0
  160. package/src/functions/document/read.ts +29 -0
  161. package/src/functions/document/update.ts +30 -0
  162. package/src/functions/entity-extraction/entity-extraction.conversations.json +1 -0
  163. package/src/functions/entity-extraction/entity-extraction.test.ts +100 -0
  164. package/src/functions/entity-extraction/entity-extraction.ts +163 -0
  165. package/src/functions/entity-extraction/index.ts +9 -0
  166. package/src/functions/exa/exa.ts +37 -0
  167. package/src/functions/exa/index.ts +6 -0
  168. package/src/functions/exa/mock.ts +71 -0
  169. package/src/functions/github/fetch-prs.ts +30 -0
  170. package/src/functions/index.ts +11 -0
  171. package/src/functions/linear/index.ts +9 -0
  172. package/src/functions/linear/linear.test.ts +86 -0
  173. package/src/functions/linear/sync-issues.ts +189 -0
  174. package/src/functions/research/create-document.ts +69 -0
  175. package/src/functions/research/graph.test.ts +69 -0
  176. package/src/functions/research/graph.ts +388 -0
  177. package/src/functions/research/index.ts +15 -0
  178. package/src/functions/research/instructions-research.tpl +98 -0
  179. package/src/functions/research/research-graph.ts +47 -0
  180. package/src/functions/research/research.conversations.json +10714 -0
  181. package/src/functions/research/research.test.ts +240 -0
  182. package/src/functions/research/research.ts +155 -0
  183. package/src/functions/research/types.ts +24 -0
  184. package/src/functions/tasks/index.ts +11 -0
  185. package/src/functions/tasks/read.ts +34 -0
  186. package/src/functions/tasks/task-list.test.ts +99 -0
  187. package/src/functions/tasks/task-list.ts +165 -0
  188. package/src/functions/tasks/update.ts +52 -0
  189. package/src/index.ts +8 -0
  190. package/src/plugins.tsx +68 -0
  191. package/src/sync/index.ts +5 -0
  192. package/src/sync/sync.ts +87 -0
  193. package/src/testing/data/exa-search-1748337321991.ts +131 -0
  194. package/src/testing/data/exa-search-1748337331526.ts +144 -0
  195. package/src/testing/data/exa-search-1748337344119.ts +133 -0
  196. package/src/testing/data/index.ts +11 -0
  197. package/src/testing/index.ts +5 -0
  198. package/src/typedefs.d.ts +8 -0
  199. package/src/util/graphql.ts +31 -0
  200. package/src/util/index.ts +5 -0
@@ -0,0 +1,163 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Toolkit from '@effect/ai/Toolkit';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Layer from 'effect/Layer';
8
+ import * as Predicate from 'effect/Predicate';
9
+ import * as Schema from 'effect/Schema';
10
+
11
+ import { AiService } from '@dxos/ai';
12
+ import { AiSession, makeToolExecutionServiceFromFunctions, makeToolResolverFromFunctions } from '@dxos/assistant';
13
+ import { Filter, Obj, Ref } from '@dxos/echo';
14
+ import { DatabaseService, FunctionInvocationService, defineFunction } from '@dxos/functions';
15
+ import { type DXN } from '@dxos/keys';
16
+ import { log } from '@dxos/log';
17
+ import { DataType } from '@dxos/schema';
18
+ import { trim } from '@dxos/util';
19
+
20
+ import { contextQueueLayerFromResearchGraph, makeGraphWriterHandler, makeGraphWriterToolkit } from '../research';
21
+
22
+ export default defineFunction({
23
+ key: 'dxos.org/functions/entity-extraction',
24
+ name: 'Entity Extraction',
25
+ description: 'Extracts entities from emails and transcripts.',
26
+ inputSchema: Schema.Struct({
27
+ source: DataType.Message.annotations({ description: 'Email or transcript to extract entities from.' }),
28
+
29
+ // TODO(dmaretskyi): Consider making this an array of blueprints instead.
30
+ instructions: Schema.optional(Schema.String).annotations({ description: 'Instructions extraction process.' }),
31
+ }),
32
+ outputSchema: Schema.Struct({
33
+ entities: Schema.optional(
34
+ Schema.Array(Obj.Any).annotations({
35
+ description: 'Extracted entities.',
36
+ }),
37
+ ),
38
+ }),
39
+ handler: Effect.fnUntraced(
40
+ function* ({ data: { source, instructions } }) {
41
+ const contact = yield* extractContact(source);
42
+ let organization: DataType.Organization | null = null;
43
+
44
+ if (contact && !contact.organization) {
45
+ const created: DXN[] = [];
46
+ const GraphWriterToolkit = makeGraphWriterToolkit({ schema: [DataType.LegacyOrganization] }).pipe();
47
+ const GraphWriterHandler = makeGraphWriterHandler(GraphWriterToolkit, {
48
+ onAppend: (dxns) => created.push(...dxns),
49
+ });
50
+ const toolkit = yield* GraphWriterToolkit.pipe(
51
+ Effect.provide(GraphWriterHandler.pipe(Layer.provide(contextQueueLayerFromResearchGraph))),
52
+ );
53
+
54
+ yield* new AiSession().run({
55
+ system: trim`
56
+ Extract the sender's organization from the email. If you are not sure, do nothing.
57
+ The extracted organization URL must match the sender's email domain.
58
+ ${instructions ? '<user_intructions>' + instructions + '</user_intructions>' : ''},
59
+ `,
60
+ prompt: JSON.stringify({ source, contact }),
61
+ toolkit,
62
+ });
63
+
64
+ if (created.length > 1) {
65
+ throw new Error('Multiple organizations created');
66
+ } else if (created.length === 1) {
67
+ organization = yield* DatabaseService.resolve(created[0], DataType.Organization);
68
+ Obj.getMeta(organization).tags ??= [];
69
+ Obj.getMeta(organization).tags!.push(...(Obj.getMeta(source)?.tags ?? []));
70
+ contact.organization = Ref.make(organization);
71
+ }
72
+ }
73
+
74
+ return {
75
+ entities: [contact, organization].filter(Predicate.isNotNullable),
76
+ };
77
+ },
78
+ Effect.provide(
79
+ Layer.mergeAll(
80
+ AiService.model('@anthropic/claude-sonnet-4-0'), // TODO(dmaretskyi): Extract.
81
+ makeToolResolverFromFunctions([], Toolkit.make()),
82
+ makeToolExecutionServiceFromFunctions(Toolkit.make() as any, Layer.empty as any),
83
+ ).pipe(
84
+ Layer.provide(
85
+ // TODO(dmaretskyi): This should be provided by environment.
86
+ Layer.mergeAll(FunctionInvocationService.layerTest()),
87
+ ),
88
+ ),
89
+ ),
90
+ ),
91
+ });
92
+
93
+ const extractContact = Effect.fn('extractContact')(function* (message: DataType.Message) {
94
+ const name = message.sender.name;
95
+ const email = message.sender.email;
96
+ if (!email) {
97
+ log.warn('email is required for contact extraction', { sender: message.sender });
98
+ return undefined;
99
+ }
100
+
101
+ const { objects: existingContacts } = yield* DatabaseService.runQuery(Filter.type(DataType.Person));
102
+
103
+ // Check for existing contact
104
+ // TODO(dmaretskyi): Query filter DSL - https://linear.app/dxos/issue/DX-541/filtercontains-should-work-with-partial-objects
105
+ const existingContact = existingContacts.find((contact) =>
106
+ contact.emails?.some((contactEmail) => contactEmail.value === email),
107
+ );
108
+
109
+ if (existingContact) {
110
+ log.info('Contact already exists', { email, existingContact });
111
+ return existingContact;
112
+ }
113
+
114
+ const newContact = Obj.make(DataType.Person, {
115
+ [Obj.Meta]: {
116
+ tags: Obj.getMeta(message)?.tags,
117
+ },
118
+ emails: [{ value: email }],
119
+ });
120
+ yield* DatabaseService.add(newContact);
121
+
122
+ if (name) {
123
+ newContact.fullName = name;
124
+ }
125
+
126
+ const emailDomain = email.split('@')[1]?.toLowerCase();
127
+ if (!emailDomain) {
128
+ log.warn('Invalid email format, cannot extract domain', { email });
129
+ return newContact;
130
+ }
131
+
132
+ log.info('extracted email domain', { emailDomain });
133
+
134
+ const { objects: existingOrganisations } = yield* DatabaseService.runQuery(Filter.type(DataType.Organization));
135
+ const matchingOrg = existingOrganisations.find((org) => {
136
+ if (org.website) {
137
+ try {
138
+ const websiteUrl =
139
+ org.website.startsWith('http://') || org.website.startsWith('https://')
140
+ ? org.website
141
+ : `https://${org.website}`;
142
+
143
+ const websiteDomain = new URL(websiteUrl).hostname.toLowerCase();
144
+ return (
145
+ websiteDomain === emailDomain ||
146
+ websiteDomain.endsWith(`.${emailDomain}`) ||
147
+ emailDomain.endsWith(`.${websiteDomain}`)
148
+ );
149
+ } catch (e) {
150
+ log.warn('Error parsing website URL', { website: org.website, error: e });
151
+ return false;
152
+ }
153
+ }
154
+ return false;
155
+ });
156
+
157
+ if (matchingOrg) {
158
+ log.info('found matching organization', { organization: matchingOrg });
159
+ newContact.organization = Ref.make(matchingOrg);
160
+ }
161
+
162
+ return newContact;
163
+ });
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { default as extract$ } from './entity-extraction';
6
+
7
+ export namespace EntityExtraction {
8
+ export const extract = extract$;
9
+ }
@@ -0,0 +1,37 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Schema from 'effect/Schema';
7
+ import Exa from 'exa-js';
8
+
9
+ import { CredentialsService, defineFunction } from '@dxos/functions';
10
+
11
+ export default defineFunction({
12
+ key: 'dxos.org/function/exa',
13
+ name: 'Exa',
14
+ description: 'Search the web for information',
15
+ inputSchema: Schema.Struct({
16
+ query: Schema.String.annotations({
17
+ description: 'The query to search for.',
18
+ }),
19
+ }),
20
+ outputSchema: Schema.Unknown,
21
+ handler: Effect.fnUntraced(function* ({ data: { query } }) {
22
+ const credential = yield* CredentialsService.getCredential({ service: 'exa.ai' });
23
+ const exa = new Exa(credential.apiKey);
24
+
25
+ const context = yield* Effect.promise(async () =>
26
+ exa.searchAndContents(query, {
27
+ type: 'auto',
28
+ text: {
29
+ maxCharacters: 3_000,
30
+ },
31
+ livecrawl: 'always',
32
+ }),
33
+ );
34
+
35
+ return context;
36
+ }),
37
+ });
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export { default as exaFunction } from './exa';
6
+ export { default as exaMockFunction } from './mock';
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Schema from 'effect/Schema';
7
+
8
+ import { defineFunction } from '@dxos/functions';
9
+
10
+ import { SEARCH_RESULTS } from '../../testing';
11
+
12
+ export default defineFunction({
13
+ key: 'dxos.org/function/exa-mock',
14
+ name: 'Exa mock',
15
+ description: 'Search the web for information',
16
+ inputSchema: Schema.Struct({
17
+ query: Schema.String.annotations({
18
+ description: 'The query to search for.',
19
+ }),
20
+ }),
21
+ outputSchema: Schema.Unknown,
22
+ handler: Effect.fnUntraced(function* ({ data: { query } }) {
23
+ const result = SEARCH_RESULTS.reduce(
24
+ (closest, current) => {
25
+ if (!current.autopromptString) {
26
+ return closest;
27
+ }
28
+ if (!closest) {
29
+ return current;
30
+ }
31
+
32
+ // Calculate Levenshtein distance
33
+ const dist1 = levenshteinDistance(query, current.autopromptString);
34
+ const dist2 = levenshteinDistance(query, closest.autopromptString || '');
35
+
36
+ // Weight by length of the longer string to normalize
37
+ const weight1 = dist1 / Math.max(query.length, current.autopromptString.length);
38
+ const weight2 = dist2 / Math.max(query.length, closest.autopromptString?.length || 0);
39
+
40
+ return weight1 < weight2 ? current : closest;
41
+ },
42
+ null as (typeof SEARCH_RESULTS)[0] | null,
43
+ );
44
+
45
+ return result;
46
+ }),
47
+ });
48
+
49
+ const levenshteinDistance = (str1: string, str2: string): number => {
50
+ const m = str1.length;
51
+ const n = str2.length;
52
+ const dp: number[][] = Array(m + 1)
53
+ .fill(null)
54
+ .map(() => Array(n + 1).fill(0));
55
+
56
+ for (let i = 0; i <= m; i++) {
57
+ dp[i][0] = i;
58
+ }
59
+ for (let j = 0; j <= n; j++) {
60
+ dp[0][j] = j;
61
+ }
62
+
63
+ for (let i = 1; i <= m; i++) {
64
+ for (let j = 1; j <= n; j++) {
65
+ dp[i][j] =
66
+ str1[i - 1] === str2[j - 1] ? dp[i - 1][j - 1] : Math.min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1;
67
+ }
68
+ }
69
+
70
+ return dp[m][n];
71
+ };
@@ -0,0 +1,30 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as HttpClient from '@effect/platform/HttpClient';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Schema from 'effect/Schema';
8
+
9
+ import { defineFunction, withAuthorization } from '@dxos/functions';
10
+
11
+ export default defineFunction({
12
+ key: 'dxos.org/function/github/fetch-prs',
13
+ name: 'Fetch PRs',
14
+ description: 'Fetches PRs from GitHub.',
15
+ inputSchema: Schema.Struct({
16
+ owner: Schema.String.annotations({
17
+ description: 'GitHub owner.',
18
+ }),
19
+ repo: Schema.String.annotations({
20
+ description: 'GitHub repository.',
21
+ }),
22
+ }),
23
+ handler: Effect.fnUntraced(function* ({ data }) {
24
+ const client = yield* HttpClient.HttpClient.pipe(Effect.map(withAuthorization({ service: 'github.com' })));
25
+
26
+ const response = yield* client.get(`https://api.github.com/repos/${data.owner}/${data.repo}/pulls`);
27
+ const json: any = yield* response.json;
28
+ return json;
29
+ }),
30
+ });
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './agent';
6
+ export * from './discord';
7
+ export * from './document';
8
+ export * from './entity-extraction';
9
+ export * from './linear';
10
+ export * from './research';
11
+ export * from './tasks';
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { default as sync$ } from './sync-issues';
6
+
7
+ export namespace Linear {
8
+ export const sync = sync$;
9
+ }
@@ -0,0 +1,86 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Toolkit from '@effect/ai/Toolkit';
6
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
7
+ import { describe, it } from '@effect/vitest';
8
+ import * as Config from 'effect/Config';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Layer from 'effect/Layer';
11
+
12
+ import { AiService } from '@dxos/ai';
13
+ import { AiServiceTestingPreset } from '@dxos/ai/testing';
14
+ import { makeToolExecutionServiceFromFunctions, makeToolResolverFromFunctions } from '@dxos/assistant';
15
+ import { Obj, Query } from '@dxos/echo';
16
+ import { TestHelpers } from '@dxos/effect';
17
+ import {
18
+ ComputeEventLogger,
19
+ CredentialsService,
20
+ DatabaseService,
21
+ FunctionInvocationService,
22
+ TracingService,
23
+ } from '@dxos/functions';
24
+ import { TestDatabaseLayer, testStoragePath } from '@dxos/functions/testing';
25
+ import { DataType } from '@dxos/schema';
26
+
27
+ import { LINEAR_ID_KEY, default as fetchLinearIssues } from './sync-issues';
28
+
29
+ const TestLayer = Layer.mergeAll(
30
+ AiService.model('@anthropic/claude-opus-4-0'),
31
+ makeToolResolverFromFunctions([], Toolkit.make()),
32
+ makeToolExecutionServiceFromFunctions(Toolkit.make() as any, Layer.empty as any),
33
+ ComputeEventLogger.layerFromTracing,
34
+ ).pipe(
35
+ Layer.provideMerge(
36
+ Layer.mergeAll(
37
+ AiServiceTestingPreset('direct'),
38
+ TestDatabaseLayer({
39
+ // indexing: { vector: true },
40
+ types: [DataType.Task, DataType.Person, DataType.Project],
41
+ storagePath: testStoragePath({ name: 'feed-test-13' }),
42
+ }),
43
+ CredentialsService.layerConfig([{ service: 'linear.app', apiKey: Config.redacted('LINEAR_API_KEY') }]),
44
+ FunctionInvocationService.layerTestMocked({ functions: [fetchLinearIssues] }).pipe(
45
+ Layer.provideMerge(ComputeEventLogger.layerFromTracing),
46
+ Layer.provideMerge(TracingService.layerLogInfo()),
47
+ ),
48
+ FetchHttpClient.layer,
49
+ ),
50
+ ),
51
+ );
52
+
53
+ describe('Linear', { timeout: 600_000 }, () => {
54
+ it.effect(
55
+ 'sync',
56
+ Effect.fnUntraced(
57
+ function* (_) {
58
+ yield* DatabaseService.flush({ indexes: true });
59
+
60
+ yield* FunctionInvocationService.invokeFunction(fetchLinearIssues, {
61
+ team: '1127c63a-6f77-4725-9229-50f6cd47321c',
62
+ });
63
+
64
+ const { objects: persons } = yield* DatabaseService.runQuery(Query.type(DataType.Person));
65
+ console.log('people', {
66
+ count: persons.length,
67
+ people: persons.map((_) => `(${_.id}) ${Obj.getLabel(_)} [${Obj.getKeys(_, LINEAR_ID_KEY)[0]?.id}]`),
68
+ });
69
+ const { objects: projects } = yield* DatabaseService.runQuery(Query.type(DataType.Project));
70
+ console.log('projects', {
71
+ count: projects.length,
72
+ projects: projects.map((_) => `(${_.id}) ${Obj.getLabel(_)} [${Obj.getKeys(_, LINEAR_ID_KEY)[0]?.id}]`),
73
+ });
74
+ const { objects: tasks } = yield* DatabaseService.runQuery(Query.type(DataType.Task));
75
+ console.log('tasks', {
76
+ count: tasks.length,
77
+ tasks: tasks.map((_) => `(${_.id}) ${Obj.getLabel(_)} [${Obj.getKeys(_, LINEAR_ID_KEY)[0]?.id}]`),
78
+ });
79
+
80
+ yield* DatabaseService.flush({ indexes: true });
81
+ },
82
+ Effect.provide(TestLayer),
83
+ TestHelpers.taggedTest('sync'),
84
+ ),
85
+ );
86
+ });
@@ -0,0 +1,189 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
+ import * as HttpClient from '@effect/platform/HttpClient';
7
+ import * as Array from 'effect/Array';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Function from 'effect/Function';
10
+ import * as Schema from 'effect/Schema';
11
+
12
+ import { Filter, Obj, Query, Ref, type Type } from '@dxos/echo';
13
+ import { DatabaseService, defineFunction, withAuthorization } from '@dxos/functions';
14
+ import { log } from '@dxos/log';
15
+ import { DataType } from '@dxos/schema';
16
+
17
+ import { syncObjects } from '../../sync';
18
+ import { graphqlRequestBody } from '../../util';
19
+
20
+ const queryIssues = `
21
+ query Issues($teamId: String!, $after: DateTimeOrDuration!) {
22
+ team(id: $teamId) {
23
+ id
24
+ name
25
+
26
+
27
+ issues(last: 150, orderBy: updatedAt, filter: {
28
+ updatedAt: { gt: $after }
29
+ }) {
30
+ edges {
31
+ node {
32
+ id
33
+ title
34
+ createdAt
35
+ updatedAt
36
+ description
37
+ assignee { id, name }
38
+ state {
39
+ name
40
+ }
41
+ project {
42
+ id
43
+ name
44
+ }
45
+ }
46
+ cursor
47
+ }
48
+ pageInfo {
49
+ hasNextPage
50
+ endCursor
51
+ }
52
+ }
53
+ }
54
+ }
55
+ `;
56
+
57
+ type LinearIssue = {
58
+ id: string;
59
+ title: string;
60
+ createdAt: string;
61
+ updatedAt: string;
62
+ description: string;
63
+ assignee: LinearPerson;
64
+ state: { name: string };
65
+ project: { id: string; name: string };
66
+ };
67
+
68
+ type LinearPerson = {
69
+ id: string;
70
+ name: string;
71
+ };
72
+
73
+ export const LINEAR_ID_KEY = 'linear.app/id';
74
+ export const LINEAR_TEAM_ID_KEY = 'linear.app/teamId';
75
+ export const LINEAR_UPDATED_AT_KEY = 'linear.app/updatedAt';
76
+
77
+ export default defineFunction({
78
+ key: 'dxos.org/function/linear/sync-issues',
79
+ name: 'Linear',
80
+ description: 'Sync issues from Linear.',
81
+ inputSchema: Schema.Struct({
82
+ team: Schema.String.annotations({
83
+ description: 'Linear team id.',
84
+ }),
85
+ }),
86
+ handler: Effect.fnUntraced(function* ({ data }) {
87
+ const client = yield* HttpClient.HttpClient.pipe(Effect.map(withAuthorization({ service: 'linear.app' })));
88
+
89
+ // Get the timestamp that was previosly synced.
90
+ const after = yield* getLatestUpdateTimestamp(data.team, DataType.Task);
91
+ log.info('will fetch', { after });
92
+
93
+ // Fetch the issues that have changed since the last sync.
94
+ const response = yield* client.post('https://api.linear.app/graphql', {
95
+ body: yield* graphqlRequestBody(queryIssues, {
96
+ teamId: data.team,
97
+ after,
98
+ }),
99
+ });
100
+ const json: any = yield* response.json;
101
+ const tasks = (json.data.team.issues.edges as any[]).map((edge: any) =>
102
+ mapLinearIssue(edge.node as LinearIssue, { teamId: data.team }),
103
+ );
104
+ log.info('Fetched tasks', { count: tasks.length });
105
+
106
+ // Synchronize new objects with ECHO.
107
+ return {
108
+ objects: yield* syncObjects(tasks, { foreignKeyId: LINEAR_ID_KEY }),
109
+ syncComplete: tasks.length < 150,
110
+ };
111
+ }, Effect.provide(FetchHttpClient.layer)),
112
+ });
113
+
114
+ const getLatestUpdateTimestamp: (
115
+ teamId: string,
116
+ dataType: Type.Obj.Any,
117
+ ) => Effect.Effect<string, never, DatabaseService> = Effect.fnUntraced(function* (teamId, dataType) {
118
+ const { objects: existingTasks } = yield* DatabaseService.runQuery(
119
+ Query.type(dataType).select(Filter.foreignKeys(dataType, [{ source: LINEAR_TEAM_ID_KEY, id: teamId }])),
120
+ );
121
+ return Function.pipe(
122
+ existingTasks,
123
+ Array.map((task) => Obj.getKeys(task, LINEAR_UPDATED_AT_KEY).at(0)?.id),
124
+ Array.filter((x) => x !== undefined),
125
+ Array.reduce('2025-01-01T00:00:00.000Z', (acc: string, x: string) => (x > acc ? x : acc)),
126
+ );
127
+ });
128
+
129
+ const mapLinearPerson = (person: LinearPerson, { teamId }: { teamId: string }): DataType.Person =>
130
+ Obj.make(DataType.Person, {
131
+ [Obj.Meta]: {
132
+ keys: [
133
+ {
134
+ id: person.id,
135
+ source: LINEAR_ID_KEY,
136
+ },
137
+ {
138
+ id: teamId,
139
+ source: LINEAR_TEAM_ID_KEY,
140
+ },
141
+ ],
142
+ },
143
+ nickname: person.name,
144
+ });
145
+
146
+ const mapLinearIssue = (issue: LinearIssue, { teamId }: { teamId: string }): DataType.Task =>
147
+ Obj.make(DataType.Task, {
148
+ [Obj.Meta]: {
149
+ keys: [
150
+ {
151
+ id: issue.id,
152
+ source: LINEAR_ID_KEY,
153
+ },
154
+ {
155
+ id: issue.updatedAt,
156
+ source: LINEAR_UPDATED_AT_KEY,
157
+ },
158
+ {
159
+ id: teamId,
160
+ source: LINEAR_TEAM_ID_KEY,
161
+ },
162
+ ],
163
+ },
164
+ title: issue.title ?? undefined,
165
+ description: issue.description ?? undefined,
166
+ assigned: !issue.assignee ? undefined : Ref.make(mapLinearPerson(issue.assignee, { teamId })),
167
+ // TODO(dmaretskyi): Sync those (+ linear team as org?).
168
+ // state: issue.state.name,
169
+
170
+ project: !issue.project
171
+ ? undefined
172
+ : Ref.make(
173
+ DataType.makeProject({
174
+ [Obj.Meta]: {
175
+ keys: [
176
+ {
177
+ id: issue.project.id,
178
+ source: LINEAR_ID_KEY,
179
+ },
180
+ {
181
+ id: teamId,
182
+ source: LINEAR_TEAM_ID_KEY,
183
+ },
184
+ ],
185
+ },
186
+ name: issue.project.name,
187
+ }),
188
+ ),
189
+ });
@@ -0,0 +1,69 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Schema from 'effect/Schema';
7
+
8
+ import { Relation } from '@dxos/echo';
9
+ import { DatabaseService, TracingService, defineFunction } from '@dxos/functions';
10
+ import { invariant } from '@dxos/invariant';
11
+ import { DXN, ObjectId } from '@dxos/keys';
12
+ import { log } from '@dxos/log';
13
+ import { Markdown } from '@dxos/plugin-markdown/types';
14
+ import { DataType } from '@dxos/schema';
15
+ import { trim } from '@dxos/util';
16
+
17
+ export default defineFunction({
18
+ key: 'dxos.org/function/research/create-document',
19
+ name: 'Create research document',
20
+ description: 'Creates a note summarizing the research.',
21
+ inputSchema: Schema.Struct({
22
+ name: Schema.String.annotations({
23
+ description: 'Name of the note.',
24
+ }),
25
+
26
+ content: Schema.String.annotations({
27
+ description: trim`
28
+ Content of the note.
29
+ Supports (and are prefered) references to research objects using @ syntax and <object> tags (refer to research blueprint instructions).
30
+ `,
31
+ }),
32
+
33
+ // TODO(dmaretskyi): Use a specialized type for this (e.g., ArtifactId renamed as RefFromLLM).
34
+ target: Schema.String.annotations({
35
+ description: trim`
36
+ Id of the object (organization, contact, etc.) for which the research was performed.
37
+ This must be a ulid.
38
+ `,
39
+ }),
40
+ }),
41
+ outputSchema: Schema.Struct({}), // TODO(burdon): Schema.Void?
42
+ handler: Effect.fnUntraced(function* ({ data: { target, name, content } }) {
43
+ log.info('Creating research document', { target, name, content });
44
+
45
+ yield* DatabaseService.flush({ indexes: true });
46
+ yield* TracingService.emitStatus({ message: 'Creating research document...' });
47
+ invariant(ObjectId.isValid(target));
48
+
49
+ const targetObj = yield* DatabaseService.resolve(DXN.fromLocalObjectId(target));
50
+
51
+ const doc = yield* DatabaseService.add(
52
+ Markdown.makeDocument({
53
+ name,
54
+ content,
55
+ }),
56
+ );
57
+ yield* DatabaseService.add(
58
+ Relation.make(DataType.HasSubject, {
59
+ [Relation.Source]: doc,
60
+ [Relation.Target]: targetObj as any,
61
+ completedAt: new Date().toISOString(),
62
+ }),
63
+ );
64
+ yield* DatabaseService.flush({ indexes: true });
65
+
66
+ log.info('Created research document', { target, name, content });
67
+ return {};
68
+ }),
69
+ });