@dxos/assistant-toolkit 0.8.4-main.1068cf700f

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 (280) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  4. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  5. package/dist/lib/neutral/index.mjs +3708 -0
  6. package/dist/lib/neutral/index.mjs.map +7 -0
  7. package/dist/lib/neutral/meta.json +1 -0
  8. package/dist/lib/neutral/testing/index.mjs +44 -0
  9. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  10. package/dist/types/src/blueprints/design/design-blueprint.d.ts +18 -0
  11. package/dist/types/src/blueprints/design/design-blueprint.d.ts.map +1 -0
  12. package/dist/types/src/blueprints/design/design-blueprint.test.d.ts +2 -0
  13. package/dist/types/src/blueprints/design/design-blueprint.test.d.ts.map +1 -0
  14. package/dist/types/src/blueprints/design/index.d.ts +3 -0
  15. package/dist/types/src/blueprints/design/index.d.ts.map +1 -0
  16. package/dist/types/src/blueprints/discord/discord-blueprint.d.ts +18 -0
  17. package/dist/types/src/blueprints/discord/discord-blueprint.d.ts.map +1 -0
  18. package/dist/types/src/blueprints/discord/index.d.ts +3 -0
  19. package/dist/types/src/blueprints/discord/index.d.ts.map +1 -0
  20. package/dist/types/src/blueprints/index.d.ts +8 -0
  21. package/dist/types/src/blueprints/index.d.ts.map +1 -0
  22. package/dist/types/src/blueprints/linear/index.d.ts +3 -0
  23. package/dist/types/src/blueprints/linear/index.d.ts.map +1 -0
  24. package/dist/types/src/blueprints/linear/linear-blueprint.d.ts +18 -0
  25. package/dist/types/src/blueprints/linear/linear-blueprint.d.ts.map +1 -0
  26. package/dist/types/src/blueprints/markdown/index.d.ts +3 -0
  27. package/dist/types/src/blueprints/markdown/index.d.ts.map +1 -0
  28. package/dist/types/src/blueprints/markdown/markdown-blueprint.d.ts +18 -0
  29. package/dist/types/src/blueprints/markdown/markdown-blueprint.d.ts.map +1 -0
  30. package/dist/types/src/blueprints/planning/index.d.ts +3 -0
  31. package/dist/types/src/blueprints/planning/index.d.ts.map +1 -0
  32. package/dist/types/src/blueprints/planning/planning-blueprint.d.ts +18 -0
  33. package/dist/types/src/blueprints/planning/planning-blueprint.d.ts.map +1 -0
  34. package/dist/types/src/blueprints/planning/planning-blueprint.test.d.ts +2 -0
  35. package/dist/types/src/blueprints/planning/planning-blueprint.test.d.ts.map +1 -0
  36. package/dist/types/src/blueprints/research/index.d.ts +3 -0
  37. package/dist/types/src/blueprints/research/index.d.ts.map +1 -0
  38. package/dist/types/src/blueprints/research/research-blueprint.d.ts +18 -0
  39. package/dist/types/src/blueprints/research/research-blueprint.d.ts.map +1 -0
  40. package/dist/types/src/blueprints/research/research-blueprint.test.d.ts +2 -0
  41. package/dist/types/src/blueprints/research/research-blueprint.test.d.ts.map +1 -0
  42. package/dist/types/src/blueprints/testing.d.ts +10 -0
  43. package/dist/types/src/blueprints/testing.d.ts.map +1 -0
  44. package/dist/types/src/blueprints/websearch/index.d.ts +4 -0
  45. package/dist/types/src/blueprints/websearch/index.d.ts.map +1 -0
  46. package/dist/types/src/blueprints/websearch/websearch-blueprint.d.ts +18 -0
  47. package/dist/types/src/blueprints/websearch/websearch-blueprint.d.ts.map +1 -0
  48. package/dist/types/src/blueprints/websearch/websearch-toolkit.d.ts +27 -0
  49. package/dist/types/src/blueprints/websearch/websearch-toolkit.d.ts.map +1 -0
  50. package/dist/types/src/chat/Chat.d.ts +31 -0
  51. package/dist/types/src/chat/Chat.d.ts.map +1 -0
  52. package/dist/types/src/chat/index.d.ts +2 -0
  53. package/dist/types/src/chat/index.d.ts.map +1 -0
  54. package/dist/types/src/crud/graph.d.ts +67 -0
  55. package/dist/types/src/crud/graph.d.ts.map +1 -0
  56. package/dist/types/src/crud/graph.test.d.ts +2 -0
  57. package/dist/types/src/crud/graph.test.d.ts.map +1 -0
  58. package/dist/types/src/crud/index.d.ts +2 -0
  59. package/dist/types/src/crud/index.d.ts.map +1 -0
  60. package/dist/types/src/experimental/feed.test.d.ts +2 -0
  61. package/dist/types/src/experimental/feed.test.d.ts.map +1 -0
  62. package/dist/types/src/functions/agent/index.d.ts +5 -0
  63. package/dist/types/src/functions/agent/index.d.ts.map +1 -0
  64. package/dist/types/src/functions/agent/prompt.d.ts +76 -0
  65. package/dist/types/src/functions/agent/prompt.d.ts.map +1 -0
  66. package/dist/types/src/functions/discord/fetch-messages.d.ts +11 -0
  67. package/dist/types/src/functions/discord/fetch-messages.d.ts.map +1 -0
  68. package/dist/types/src/functions/discord/fetch-messages.test.d.ts +2 -0
  69. package/dist/types/src/functions/discord/fetch-messages.test.d.ts.map +1 -0
  70. package/dist/types/src/functions/discord/index.d.ts +12 -0
  71. package/dist/types/src/functions/discord/index.d.ts.map +1 -0
  72. package/dist/types/src/functions/document/create.d.ts +6 -0
  73. package/dist/types/src/functions/document/create.d.ts.map +1 -0
  74. package/dist/types/src/functions/document/index.d.ts +30 -0
  75. package/dist/types/src/functions/document/index.d.ts.map +1 -0
  76. package/dist/types/src/functions/document/read.d.ts +14 -0
  77. package/dist/types/src/functions/document/read.d.ts.map +1 -0
  78. package/dist/types/src/functions/document/update.d.ts +13 -0
  79. package/dist/types/src/functions/document/update.d.ts.map +1 -0
  80. package/dist/types/src/functions/entity-extraction/entity-extraction.d.ts +174 -0
  81. package/dist/types/src/functions/entity-extraction/entity-extraction.d.ts.map +1 -0
  82. package/dist/types/src/functions/entity-extraction/entity-extraction.test.d.ts +2 -0
  83. package/dist/types/src/functions/entity-extraction/entity-extraction.test.d.ts.map +1 -0
  84. package/dist/types/src/functions/entity-extraction/index.d.ts +175 -0
  85. package/dist/types/src/functions/entity-extraction/index.d.ts.map +1 -0
  86. package/dist/types/src/functions/exa/data/exa-search-1748337321991.d.ts +38 -0
  87. package/dist/types/src/functions/exa/data/exa-search-1748337321991.d.ts.map +1 -0
  88. package/dist/types/src/functions/exa/data/exa-search-1748337331526.d.ts +37 -0
  89. package/dist/types/src/functions/exa/data/exa-search-1748337331526.d.ts.map +1 -0
  90. package/dist/types/src/functions/exa/data/exa-search-1748337344119.d.ts +58 -0
  91. package/dist/types/src/functions/exa/data/exa-search-1748337344119.d.ts.map +1 -0
  92. package/dist/types/src/functions/exa/data/index.d.ts +3 -0
  93. package/dist/types/src/functions/exa/data/index.d.ts.map +1 -0
  94. package/dist/types/src/functions/exa/exa.d.ts +5 -0
  95. package/dist/types/src/functions/exa/exa.d.ts.map +1 -0
  96. package/dist/types/src/functions/exa/index.d.ts +3 -0
  97. package/dist/types/src/functions/exa/index.d.ts.map +1 -0
  98. package/dist/types/src/functions/exa/mock.d.ts +5 -0
  99. package/dist/types/src/functions/exa/mock.d.ts.map +1 -0
  100. package/dist/types/src/functions/github/fetch-prs.d.ts +6 -0
  101. package/dist/types/src/functions/github/fetch-prs.d.ts.map +1 -0
  102. package/dist/types/src/functions/index.d.ts +8 -0
  103. package/dist/types/src/functions/index.d.ts.map +1 -0
  104. package/dist/types/src/functions/linear/index.d.ts +9 -0
  105. package/dist/types/src/functions/linear/index.d.ts.map +1 -0
  106. package/dist/types/src/functions/linear/linear.test.d.ts +2 -0
  107. package/dist/types/src/functions/linear/linear.test.d.ts.map +1 -0
  108. package/dist/types/src/functions/linear/sync-issues.d.ts +12 -0
  109. package/dist/types/src/functions/linear/sync-issues.d.ts.map +1 -0
  110. package/dist/types/src/functions/research/document-create.d.ts +9 -0
  111. package/dist/types/src/functions/research/document-create.d.ts.map +1 -0
  112. package/dist/types/src/functions/research/index.d.ts +21 -0
  113. package/dist/types/src/functions/research/index.d.ts.map +1 -0
  114. package/dist/types/src/functions/research/research-graph.d.ts +19 -0
  115. package/dist/types/src/functions/research/research-graph.d.ts.map +1 -0
  116. package/dist/types/src/functions/research/research.d.ts +14 -0
  117. package/dist/types/src/functions/research/research.d.ts.map +1 -0
  118. package/dist/types/src/functions/research/research.test.d.ts +2 -0
  119. package/dist/types/src/functions/research/research.test.d.ts.map +1 -0
  120. package/dist/types/src/functions/research/types.d.ts +6 -0
  121. package/dist/types/src/functions/research/types.d.ts.map +1 -0
  122. package/dist/types/src/functions/tasks/index.d.ts +29 -0
  123. package/dist/types/src/functions/tasks/index.d.ts.map +1 -0
  124. package/dist/types/src/functions/tasks/read.d.ts +14 -0
  125. package/dist/types/src/functions/tasks/read.d.ts.map +1 -0
  126. package/dist/types/src/functions/tasks/task-list.d.ts +74 -0
  127. package/dist/types/src/functions/tasks/task-list.d.ts.map +1 -0
  128. package/dist/types/src/functions/tasks/task-list.test.d.ts +2 -0
  129. package/dist/types/src/functions/tasks/task-list.test.d.ts.map +1 -0
  130. package/dist/types/src/functions/tasks/update.d.ts +16 -0
  131. package/dist/types/src/functions/tasks/update.d.ts.map +1 -0
  132. package/dist/types/src/index.d.ts +9 -0
  133. package/dist/types/src/index.d.ts.map +1 -0
  134. package/dist/types/src/initiative/Initiative.d.ts +41 -0
  135. package/dist/types/src/initiative/Initiative.d.ts.map +1 -0
  136. package/dist/types/src/initiative/blueprint.d.ts +153 -0
  137. package/dist/types/src/initiative/blueprint.d.ts.map +1 -0
  138. package/dist/types/src/initiative/functions/addArtifact.d.ts +6 -0
  139. package/dist/types/src/initiative/functions/addArtifact.d.ts.map +1 -0
  140. package/dist/types/src/initiative/functions/agent.d.ts +62 -0
  141. package/dist/types/src/initiative/functions/agent.d.ts.map +1 -0
  142. package/dist/types/src/initiative/functions/getContext.d.ts +13 -0
  143. package/dist/types/src/initiative/functions/getContext.d.ts.map +1 -0
  144. package/dist/types/src/initiative/functions/index.d.ts +5 -0
  145. package/dist/types/src/initiative/functions/index.d.ts.map +1 -0
  146. package/dist/types/src/initiative/functions/qualifier.d.ts +60 -0
  147. package/dist/types/src/initiative/functions/qualifier.d.ts.map +1 -0
  148. package/dist/types/src/initiative/index.d.ts +6 -0
  149. package/dist/types/src/initiative/index.d.ts.map +1 -0
  150. package/dist/types/src/initiative/initiative.test.d.ts +2 -0
  151. package/dist/types/src/initiative/initiative.test.d.ts.map +1 -0
  152. package/dist/types/src/initiative/plan.d.ts +77 -0
  153. package/dist/types/src/initiative/plan.d.ts.map +1 -0
  154. package/dist/types/src/initiative/util.d.ts +29 -0
  155. package/dist/types/src/initiative/util.d.ts.map +1 -0
  156. package/dist/types/src/planning/blueprint.d.ts +18 -0
  157. package/dist/types/src/planning/blueprint.d.ts.map +1 -0
  158. package/dist/types/src/planning/functions/index.d.ts +2 -0
  159. package/dist/types/src/planning/functions/index.d.ts.map +1 -0
  160. package/dist/types/src/planning/functions/update-tasks.d.ts +9 -0
  161. package/dist/types/src/planning/functions/update-tasks.d.ts.map +1 -0
  162. package/dist/types/src/planning/index.d.ts +3 -0
  163. package/dist/types/src/planning/index.d.ts.map +1 -0
  164. package/dist/types/src/sync/index.d.ts +2 -0
  165. package/dist/types/src/sync/index.d.ts.map +1 -0
  166. package/dist/types/src/sync/sync.d.ts +15 -0
  167. package/dist/types/src/sync/sync.d.ts.map +1 -0
  168. package/dist/types/src/testing/index.d.ts +2 -0
  169. package/dist/types/src/testing/index.d.ts.map +1 -0
  170. package/dist/types/src/testing/plugins.d.ts +19 -0
  171. package/dist/types/src/testing/plugins.d.ts.map +1 -0
  172. package/dist/types/src/toolkits/AssistantToolkit.d.ts +43 -0
  173. package/dist/types/src/toolkits/AssistantToolkit.d.ts.map +1 -0
  174. package/dist/types/src/toolkits/AssistantToolkit.test.d.ts +2 -0
  175. package/dist/types/src/toolkits/AssistantToolkit.test.d.ts.map +1 -0
  176. package/dist/types/src/toolkits/SystemToolkit.d.ts +99 -0
  177. package/dist/types/src/toolkits/SystemToolkit.d.ts.map +1 -0
  178. package/dist/types/src/toolkits/WebToolkit.d.ts +38 -0
  179. package/dist/types/src/toolkits/WebToolkit.d.ts.map +1 -0
  180. package/dist/types/src/toolkits/index.d.ts +4 -0
  181. package/dist/types/src/toolkits/index.d.ts.map +1 -0
  182. package/dist/types/src/util/graphql.d.ts +22 -0
  183. package/dist/types/src/util/graphql.d.ts.map +1 -0
  184. package/dist/types/src/util/index.d.ts +2 -0
  185. package/dist/types/src/util/index.d.ts.map +1 -0
  186. package/dist/types/tsconfig.tsbuildinfo +1 -0
  187. package/package.json +74 -0
  188. package/src/blueprints/design/design-blueprint.test.ts +100 -0
  189. package/src/blueprints/design/design-blueprint.ts +31 -0
  190. package/src/blueprints/design/index.ts +7 -0
  191. package/src/blueprints/discord/discord-blueprint.ts +32 -0
  192. package/src/blueprints/discord/index.ts +7 -0
  193. package/src/blueprints/index.ts +11 -0
  194. package/src/blueprints/linear/index.ts +7 -0
  195. package/src/blueprints/linear/linear-blueprint.ts +33 -0
  196. package/src/blueprints/markdown/index.ts +7 -0
  197. package/src/blueprints/markdown/markdown-blueprint.ts +24 -0
  198. package/src/blueprints/planning/index.ts +7 -0
  199. package/src/blueprints/planning/planning-blueprint.test.ts +120 -0
  200. package/src/blueprints/planning/planning-blueprint.ts +96 -0
  201. package/src/blueprints/research/index.ts +7 -0
  202. package/src/blueprints/research/research-blueprint.test.ts +7 -0
  203. package/src/blueprints/research/research-blueprint.ts +51 -0
  204. package/src/blueprints/testing.ts +30 -0
  205. package/src/blueprints/websearch/index.ts +9 -0
  206. package/src/blueprints/websearch/websearch-blueprint.ts +18 -0
  207. package/src/blueprints/websearch/websearch-toolkit.ts +8 -0
  208. package/src/chat/Chat.ts +45 -0
  209. package/src/chat/index.ts +5 -0
  210. package/src/crud/graph.test.ts +46 -0
  211. package/src/crud/graph.ts +380 -0
  212. package/src/crud/index.ts +5 -0
  213. package/src/experimental/feed.test.ts +106 -0
  214. package/src/functions/agent/index.ts +11 -0
  215. package/src/functions/agent/prompt.ts +116 -0
  216. package/src/functions/discord/fetch-messages.test.ts +54 -0
  217. package/src/functions/discord/fetch-messages.ts +252 -0
  218. package/src/functions/discord/index.ts +9 -0
  219. package/src/functions/document/create.ts +29 -0
  220. package/src/functions/document/index.ts +13 -0
  221. package/src/functions/document/read.ts +32 -0
  222. package/src/functions/document/update.ts +32 -0
  223. package/src/functions/entity-extraction/entity-extraction.conversations.json +1 -0
  224. package/src/functions/entity-extraction/entity-extraction.test.ts +74 -0
  225. package/src/functions/entity-extraction/entity-extraction.ts +181 -0
  226. package/src/functions/entity-extraction/index.ts +9 -0
  227. package/src/functions/exa/data/exa-search-1748337321991.ts +131 -0
  228. package/src/functions/exa/data/exa-search-1748337331526.ts +144 -0
  229. package/src/functions/exa/data/exa-search-1748337344119.ts +133 -0
  230. package/src/functions/exa/data/index.ts +11 -0
  231. package/src/functions/exa/exa.ts +37 -0
  232. package/src/functions/exa/index.ts +6 -0
  233. package/src/functions/exa/mock.ts +71 -0
  234. package/src/functions/github/fetch-prs.ts +31 -0
  235. package/src/functions/index.ts +11 -0
  236. package/src/functions/linear/index.ts +9 -0
  237. package/src/functions/linear/linear.test.ts +58 -0
  238. package/src/functions/linear/sync-issues.ts +191 -0
  239. package/src/functions/research/document-create.ts +73 -0
  240. package/src/functions/research/index.ts +14 -0
  241. package/src/functions/research/research-graph.ts +49 -0
  242. package/src/functions/research/research-instructions.tpl +106 -0
  243. package/src/functions/research/research.conversations.json +1 -0
  244. package/src/functions/research/research.test.ts +144 -0
  245. package/src/functions/research/research.ts +182 -0
  246. package/src/functions/research/types.ts +26 -0
  247. package/src/functions/tasks/index.ts +11 -0
  248. package/src/functions/tasks/read.ts +34 -0
  249. package/src/functions/tasks/task-list.test.ts +99 -0
  250. package/src/functions/tasks/task-list.ts +165 -0
  251. package/src/functions/tasks/update.ts +52 -0
  252. package/src/index.ts +12 -0
  253. package/src/initiative/Initiative.ts +61 -0
  254. package/src/initiative/blueprint.ts +60 -0
  255. package/src/initiative/functions/addArtifact.ts +41 -0
  256. package/src/initiative/functions/agent.ts +77 -0
  257. package/src/initiative/functions/getContext.ts +54 -0
  258. package/src/initiative/functions/index.ts +8 -0
  259. package/src/initiative/functions/qualifier.ts +98 -0
  260. package/src/initiative/index.ts +9 -0
  261. package/src/initiative/initiative.test.ts +584 -0
  262. package/src/initiative/plan.ts +107 -0
  263. package/src/initiative/util.ts +137 -0
  264. package/src/planning/blueprint.ts +14 -0
  265. package/src/planning/functions/index.ts +5 -0
  266. package/src/planning/functions/update-tasks.ts +168 -0
  267. package/src/planning/index.ts +6 -0
  268. package/src/sync/index.ts +5 -0
  269. package/src/sync/sync.ts +95 -0
  270. package/src/testing/index.ts +5 -0
  271. package/src/testing/plugins.tsx +69 -0
  272. package/src/toolkits/AssistantToolkit.conversations.json +1 -0
  273. package/src/toolkits/AssistantToolkit.test.ts +72 -0
  274. package/src/toolkits/AssistantToolkit.ts +70 -0
  275. package/src/toolkits/SystemToolkit.ts +299 -0
  276. package/src/toolkits/WebToolkit.ts +34 -0
  277. package/src/toolkits/index.ts +7 -0
  278. package/src/typedefs.d.ts +8 -0
  279. package/src/util/graphql.ts +31 -0
  280. package/src/util/index.ts +5 -0
@@ -0,0 +1,116 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Array from 'effect/Array';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Function from 'effect/Function';
8
+ import * as Match from 'effect/Match';
9
+ import * as Option from 'effect/Option';
10
+ import * as Schema from 'effect/Schema';
11
+
12
+ import { AiService, ConsolePrinter, ModelName } from '@dxos/ai';
13
+ import { AiSession, GenerationObserver, createToolkit } from '@dxos/assistant';
14
+ import { Prompt, Template } from '@dxos/blueprints';
15
+ import { Obj, Ref, Type } from '@dxos/echo';
16
+ import { Database } from '@dxos/echo';
17
+ import { TracingService, defineFunction } from '@dxos/functions';
18
+ import { log } from '@dxos/log';
19
+
20
+ const DEFAULT_MODEL: ModelName = '@anthropic/claude-opus-4-0';
21
+
22
+ export default defineFunction({
23
+ key: 'dxos.org/function/prompt',
24
+ name: 'Agent',
25
+ description: 'Agentic worker that executes a provided prompt using blueprints and tools.',
26
+ inputSchema: Schema.Struct({
27
+ prompt: Type.Ref(Prompt.Prompt),
28
+ systemPrompt: Type.Ref(Prompt.Prompt).pipe(Schema.optional),
29
+ /**
30
+ * @default @anthropic/claude-opus-4-0
31
+ */
32
+ model: Schema.optional(ModelName),
33
+ /**
34
+ * Input object or data.
35
+ * References get auto-resolved.
36
+ */
37
+ input: Schema.Any.pipe(Schema.annotations({ title: 'Input' })),
38
+ }),
39
+ outputSchema: Schema.Any,
40
+ handler: Effect.fnUntraced(function* ({ data }) {
41
+ log.info('processing input', { input: data.input });
42
+
43
+ // TODO(wittjosiah): Support templated object as input.
44
+ // Currently the object templating only supports direct pass-through or strings.
45
+ // const input = { ...data.input };
46
+ // for (const key of Object.keys(data.input)) {
47
+ // const value = data.input[key];
48
+ // if (Ref.isRef(value)) {
49
+ // const object = yield* Database.load(value);
50
+ // input[key] = Obj.toJSON(object);
51
+ // } else {
52
+ // input[key] = JSON.stringify(value);
53
+ // }
54
+ // }
55
+ const input = yield* Match.value(data.input).pipe(
56
+ Match.when(
57
+ (value: any) => Ref.isRef(value),
58
+ Effect.fnUntraced(function* (ref) {
59
+ const object = yield* Database.load(ref);
60
+ return Obj.toJSON(object as Obj.Unknown);
61
+ }),
62
+ ),
63
+ Match.orElse(() => Effect.succeed(data.input)),
64
+ );
65
+
66
+ yield* Database.flush({ indexes: true });
67
+ const prompt = yield* Database.load(data.prompt);
68
+ const systemPrompt = data.systemPrompt ? yield* Database.load(data.systemPrompt) : undefined;
69
+ yield* TracingService.emitStatus({ message: `Running ${prompt.id}` });
70
+
71
+ log.info('starting agent', { prompt: prompt.id, input });
72
+
73
+ const blueprints = yield* Function.pipe(
74
+ prompt.blueprints,
75
+ Array.appendAll(systemPrompt?.blueprints ?? []),
76
+ Effect.forEach(Database.loadOption),
77
+ Effect.map(Array.filter(Option.isSome)),
78
+ Effect.map(Array.map((option) => option.value)),
79
+ );
80
+ const toolkit = yield* createToolkit({ blueprints });
81
+
82
+ const objects = yield* Function.pipe(
83
+ prompt.context,
84
+ Array.appendAll(systemPrompt?.context ?? []),
85
+ Effect.forEach(Database.loadOption),
86
+ Effect.map(Array.filter(Option.isSome)),
87
+ Effect.map(Array.map((option) => option.value)),
88
+ );
89
+
90
+ const promptInstructions = yield* Database.load(prompt.instructions.source);
91
+ const promptText = Template.process(promptInstructions.content, input);
92
+
93
+ const systemInstructions = systemPrompt ? yield* Database.load(systemPrompt.instructions.source) : undefined;
94
+ const systemText = systemInstructions ? Template.process(systemInstructions.content, {}) : undefined;
95
+
96
+ const session = new AiSession();
97
+ const result = yield* session
98
+ .run({
99
+ prompt: promptText,
100
+ system: systemText,
101
+ blueprints,
102
+ objects: objects as Obj.Unknown[],
103
+ toolkit,
104
+ observer: GenerationObserver.fromPrinter(new ConsolePrinter({ tag: 'agent' })),
105
+ })
106
+ .pipe(Effect.provide(AiService.model(data.model ?? DEFAULT_MODEL)));
107
+ const lastBlock = result
108
+ .at(-1)
109
+ ?.blocks.filter((block) => block._tag === 'text')
110
+ .at(-1);
111
+
112
+ return {
113
+ note: lastBlock?.text,
114
+ };
115
+ }),
116
+ });
@@ -0,0 +1,54 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
+ import { describe, it } from '@effect/vitest';
7
+ import * as Config from 'effect/Config';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as Layer from 'effect/Layer';
10
+
11
+ import { AiService } from '@dxos/ai';
12
+ import { AiServiceTestingPreset } from '@dxos/ai/testing';
13
+ import { GenericToolkit, ToolExecutionServices } from '@dxos/assistant';
14
+ import { TestHelpers } from '@dxos/effect/testing';
15
+ import { CredentialsService, FunctionInvocationService, TracingService } from '@dxos/functions';
16
+ import { FunctionInvocationServiceLayerTestMocked, TestDatabaseLayer } from '@dxos/functions-runtime/testing';
17
+
18
+ import { default as fetchMessages } from './fetch-messages';
19
+
20
+ const TestLayer = Layer.mergeAll(AiService.model('@anthropic/claude-opus-4-0'), ToolExecutionServices).pipe(
21
+ Layer.provideMerge(
22
+ Layer.mergeAll(
23
+ GenericToolkit.providerEmpty,
24
+ AiServiceTestingPreset('direct'),
25
+ TestDatabaseLayer({}),
26
+ CredentialsService.layerConfig([{ service: 'discord.com', apiKey: Config.redacted('DISCORD_TOKEN') }]),
27
+ FetchHttpClient.layer,
28
+ FunctionInvocationServiceLayerTestMocked({ functions: [fetchMessages] }).pipe(
29
+ Layer.provideMerge(TracingService.layerNoop),
30
+ ),
31
+ ),
32
+ ),
33
+ );
34
+
35
+ const DXOS_SERVER_ID = '837138313172353095';
36
+
37
+ describe('Feed', { timeout: 600_000 }, () => {
38
+ it.effect(
39
+ 'fetch discord messages',
40
+ Effect.fnUntraced(
41
+ function* (_) {
42
+ const messages = yield* FunctionInvocationService.invokeFunction(fetchMessages, {
43
+ serverId: DXOS_SERVER_ID,
44
+ // channelId: '1404487604761526423',
45
+ last: '7d',
46
+ });
47
+ console.log(messages);
48
+ },
49
+ Effect.provide(TestLayer),
50
+ TestHelpers.provideTestContext,
51
+ TestHelpers.taggedTest('sync'),
52
+ ),
53
+ );
54
+ });
@@ -0,0 +1,252 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as FetchHttpClient from '@effect/platform/FetchHttpClient';
6
+ import { DiscordConfig, DiscordREST, DiscordRESTMemoryLive } from 'dfx';
7
+ import type {
8
+ GuildChannelResponse,
9
+ MessageResponse,
10
+ PrivateChannelResponse,
11
+ PrivateGroupChannelResponse,
12
+ ThreadResponse,
13
+ } from 'dfx/types';
14
+ import * as Array from 'effect/Array';
15
+ import * as Effect from 'effect/Effect';
16
+ import * as Function from 'effect/Function';
17
+ import * as Layer from 'effect/Layer';
18
+ import * as Option from 'effect/Option';
19
+ import * as Schema from 'effect/Schema';
20
+
21
+ import { Obj } from '@dxos/echo';
22
+ import { CredentialsService, TracingService, defineFunction } from '@dxos/functions';
23
+ import { log } from '@dxos/log';
24
+ import { Message } from '@dxos/types';
25
+
26
+ // TODO(dmaretskyi): Extract.
27
+ const TimeRange = class extends Schema.String.pipe(Schema.pattern(/\d+(s|m|h|d)/)).annotations({
28
+ description: 'Time range. 1d - 1 day, 2h - 2 hours, 30m - 30 minutes, 15s - 15 seconds.',
29
+ examples: ['1d', '2h', '30m', '15s'],
30
+ }) {
31
+ static toSeconds(timeRange: Schema.Schema.Type<typeof TimeRange>) {
32
+ const match = timeRange.match(/(\d+)(s|m|h|d)/);
33
+ if (!match) {
34
+ throw new Error(`Invalid time range: ${timeRange}`);
35
+ }
36
+ const [_, amount, unit] = match;
37
+ switch (unit) {
38
+ case 's':
39
+ return Number(amount);
40
+ case 'm':
41
+ return Number(amount) * 60;
42
+ case 'h':
43
+ return Number(amount) * 60 * 60;
44
+ case 'd':
45
+ return Number(amount) * 24 * 60 * 60;
46
+ default:
47
+ throw new Error(`Invalid time range unit: ${unit}`);
48
+ }
49
+ }
50
+ };
51
+ type TimeRange = Schema.Schema.Type<typeof TimeRange>;
52
+
53
+ const DiscordConfigFromCredential = Layer.unwrapEffect(
54
+ Effect.gen(function* () {
55
+ return DiscordConfig.layer({
56
+ token: yield* CredentialsService.getApiKey({ service: 'discord.com' }),
57
+ rest: {
58
+ baseUrl: 'https://api-proxy.dxos.workers.dev/discord.com/api/v10',
59
+ },
60
+ });
61
+ }),
62
+ );
63
+
64
+ type DiscordChannel = GuildChannelResponse | PrivateChannelResponse | PrivateGroupChannelResponse | ThreadResponse;
65
+
66
+ const DEFAULT_AFTER = 1704067200; // 2024-01-01
67
+ const DEFAULT_LIMIT = 500;
68
+ const DEFAULT_IGNORE_USERNAMES = ['GitHub', 'Needle'];
69
+
70
+ // TODO(dmaretskyi): Align with standard thread type.
71
+ type Thread = {
72
+ discordChannelId: string;
73
+ name?: string;
74
+ messages: Message.Message[];
75
+ };
76
+
77
+ export default defineFunction({
78
+ key: 'dxos.org/function/fetch-discord-messages',
79
+ name: 'Sync Discord messages',
80
+ inputSchema: Schema.Struct({
81
+ serverId: Schema.String.annotations({
82
+ description: 'The ID of the server to fetch messages from.',
83
+ }),
84
+ channelId: Schema.optional(Schema.String).annotations({
85
+ description:
86
+ 'The ID of the channel to fetch messages from. Will crawl all channels from the server if not specified.',
87
+ }),
88
+ after: Schema.optional(Schema.Number).annotations({
89
+ description:
90
+ 'Fetch messages that were sent after a given date. Unix timestamp in seconds. Exclusive with `last`.',
91
+ }),
92
+ last: TimeRange.annotations({
93
+ description:
94
+ 'Time range to fetch most recent messages. Specifies the range in the past, from now. "1d" would fetch messages from the last 24 hours.',
95
+ }),
96
+ limit: Schema.optional(Schema.Number).annotations({
97
+ description: 'The maximum number of messages to fetch.',
98
+ }),
99
+ pageSize: Schema.optional(Schema.Number).annotations({
100
+ description: 'The number of messages to fetch per page.',
101
+ }),
102
+ ignoreUsernames: Schema.optional(Schema.Array(Schema.String)).annotations({
103
+ description: 'Exclude messages from these usernames.',
104
+ }),
105
+ }),
106
+ handler: Effect.fnUntraced(
107
+ function* ({
108
+ data: {
109
+ serverId,
110
+ channelId,
111
+ after,
112
+ last,
113
+ pageSize = 100,
114
+ limit = DEFAULT_LIMIT,
115
+ ignoreUsernames = DEFAULT_IGNORE_USERNAMES,
116
+ },
117
+ }) {
118
+ if (!after && !last) {
119
+ throw new Error('cannot specify both `after` and `last`');
120
+ }
121
+ const afterTs = last ? Date.now() / 1000 - TimeRange.toSeconds(last) : (after ?? DEFAULT_AFTER);
122
+
123
+ const rest = yield* DiscordREST;
124
+
125
+ let channels: DiscordChannel[] = [];
126
+ channels.push(...(yield* rest.listGuildChannels(serverId)));
127
+ const { threads: guildThreads } = yield* rest.getActiveGuildThreads(serverId);
128
+ channels.push(...guildThreads);
129
+ if (channelId) {
130
+ channels = channels.filter((channel) => channel.id === channelId);
131
+ }
132
+ if (channels.length === 0) {
133
+ throw new Error('no channels found');
134
+ }
135
+ for (const channel of channels) {
136
+ log.info('channel', { id: channel.id, name: 'name' in channel ? channel.name : undefined });
137
+ }
138
+
139
+ yield* TracingService.emitStatus({ message: `Will fetch from channels: ${channels.length}` });
140
+
141
+ const threads = yield* Effect.forEach(
142
+ channels,
143
+ Effect.fnUntraced(function* (channel) {
144
+ const allMessages: Message.Message[] = [];
145
+
146
+ let lastMessage: Option.Option<Message.Message> = Option.none();
147
+ while (true) {
148
+ const { id: lastId = undefined } = Function.pipe(
149
+ lastMessage,
150
+ Option.map(Obj.getKeys('discord.com')),
151
+ Option.flatMap(Option.fromIterable),
152
+ Option.getOrElse(() => ({ id: undefined })),
153
+ );
154
+
155
+ const options = {
156
+ after: !lastId ? `${generateSnowflake(afterTs)}` : lastId,
157
+ limit: pageSize,
158
+ };
159
+ log.info('fetching messages', {
160
+ lastId,
161
+ afterTs,
162
+ afterSnowflake: options.after,
163
+ after: parseSnowflake(options.after),
164
+ limit: options.limit,
165
+ });
166
+ const messages = yield* rest.listMessages(channel.id, options).pipe(
167
+ Effect.map(Array.map(makeMessage)),
168
+ Effect.map(Array.reverse),
169
+ Effect.catchTag('ErrorResponse', (err) =>
170
+ err.cause.code === 50001 ? Effect.succeed([]) : Effect.fail(err),
171
+ ),
172
+ );
173
+ if (messages.length > 0) {
174
+ lastMessage = Option.fromNullable(messages.at(-1));
175
+ allMessages.push(...messages);
176
+ } else {
177
+ break;
178
+ }
179
+ yield* TracingService.emitStatus({ message: `Fetched messages: ${allMessages.length}` });
180
+ if (allMessages.length >= limit) {
181
+ break;
182
+ }
183
+ }
184
+
185
+ return {
186
+ discordChannelId: channel.id,
187
+ name: 'name' in channel ? (channel.name ?? undefined) : undefined,
188
+ messages: allMessages
189
+ .filter((message) => !message.sender.name || !ignoreUsernames.includes(message.sender.name))
190
+ .filter((message) =>
191
+ message.blocks.some((block: any) => block._tag === 'text' && block.text.trim().length > 0),
192
+ ),
193
+ } satisfies Thread;
194
+ }),
195
+ { concurrency: 10 },
196
+ );
197
+
198
+ return threads
199
+ .filter((thread) => thread.messages.length > 0)
200
+ .map(serializeThread)
201
+ .join('\n');
202
+ },
203
+ Effect.provide(
204
+ DiscordRESTMemoryLive.pipe(Layer.provideMerge(DiscordConfigFromCredential)).pipe(
205
+ Layer.provide(FetchHttpClient.layer),
206
+ ),
207
+ ),
208
+ Effect.orDie,
209
+ ),
210
+ });
211
+
212
+ /**
213
+ * @param unixTimestamp in seconds
214
+ */
215
+ const generateSnowflake = (unixTimestamp: number): bigint => {
216
+ const discordEpoch = 1420070400000n; // Discord Epoch (ms)
217
+ return (BigInt(unixTimestamp * 1000) - discordEpoch) << 22n;
218
+ };
219
+
220
+ const parseSnowflake = (snowflake: string): Date => {
221
+ const discordEpoch = 1420070400000n; // Discord Epoch (ms)
222
+ return new Date(Number((BigInt(snowflake) >> 22n) + discordEpoch));
223
+ };
224
+
225
+ // TODO(burdon): Move to @dxos/types.
226
+ const makeMessage = (message: MessageResponse): Message.Message =>
227
+ Obj.make(Message.Message, {
228
+ [Obj.Meta]: {
229
+ keys: [
230
+ { id: message.id, source: 'discord.com' },
231
+ { id: message.channel_id, source: 'discord.com/thread' },
232
+ ],
233
+ },
234
+ sender: { name: message.author.username },
235
+ created: message.timestamp,
236
+ blocks: [{ _tag: 'text', text: message.content }],
237
+ });
238
+
239
+ /**
240
+ * Standard JSON serialization is to verbose for large amounts of data.
241
+ */
242
+ const serializeThread = (thread: Thread): string => {
243
+ return `<thread id=${thread.discordChannelId} name=${thread.name ?? ''}>\n${thread.messages
244
+ .map(
245
+ (message) =>
246
+ ` ${message.sender.name}: ${message.blocks
247
+ .filter((block: any) => block._tag === 'text')
248
+ .map((block: any) => block.text)
249
+ .join(' ')}`,
250
+ )
251
+ .join('\n')}\n</thread>`;
252
+ };
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { default as fetch$ } from './fetch-messages';
6
+
7
+ export namespace Discord {
8
+ export const fetch = fetch$;
9
+ }
@@ -0,0 +1,29 @@
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 { Database, Ref } from '@dxos/echo';
9
+ import { defineFunction } from '@dxos/functions';
10
+ import { Markdown } from '@dxos/plugin-markdown/types';
11
+
12
+ export default defineFunction({
13
+ key: 'dxos.org/function/markdown/create',
14
+ name: 'Create markdown document',
15
+ description: 'Creates a new markdown document.',
16
+ inputSchema: Schema.Struct({
17
+ name: Schema.String.annotations({
18
+ description: 'Name of the document.',
19
+ }),
20
+ content: Schema.String.annotations({
21
+ description: 'Content of the document.',
22
+ }),
23
+ }),
24
+ outputSchema: Schema.Void,
25
+ handler: Effect.fn(function* ({ data: { name, content } }) {
26
+ const doc = yield* Database.add(Markdown.make({ name, content }));
27
+ return { document: Ref.make(doc) };
28
+ }),
29
+ });
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { default as create$ } from './create';
6
+ import { default as read$ } from './read';
7
+ import { default as update$ } from './update';
8
+
9
+ export namespace Document {
10
+ export const read = read$;
11
+ export const update = update$;
12
+ export const create = create$;
13
+ }
@@ -0,0 +1,32 @@
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 { Database, Type } from '@dxos/echo';
9
+ import { defineFunction } from '@dxos/functions';
10
+ import { Markdown } from '@dxos/plugin-markdown/types';
11
+
12
+ export default defineFunction({
13
+ key: 'dxos.org/function/markdown/read',
14
+ name: 'Read markdown document',
15
+ description:
16
+ 'Read markdown document. Note that result is a snapshot in time, and might have changed since the document was last read.',
17
+ inputSchema: Schema.Struct({
18
+ document: Type.Ref(Markdown.Document).annotations({
19
+ description: 'The document to read.',
20
+ }),
21
+ }),
22
+ outputSchema: Schema.Struct({
23
+ content: Schema.String,
24
+ }),
25
+ handler: Effect.fn(function* ({ data: { document } }) {
26
+ const { content } = yield* document.pipe(
27
+ Database.load,
28
+ Effect.flatMap((doc) => doc.content.pipe(Database.load)),
29
+ );
30
+ return { content };
31
+ }),
32
+ });
@@ -0,0 +1,32 @@
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 { Database, Obj, Type } from '@dxos/echo';
9
+ import { defineFunction } from '@dxos/functions';
10
+ import { Markdown } from '@dxos/plugin-markdown/types';
11
+
12
+ export default defineFunction({
13
+ key: 'dxos.org/function/markdown/update',
14
+ name: 'Update markdown',
15
+ description: 'Updates the entire contents of the markdown document.',
16
+ inputSchema: Schema.Struct({
17
+ doc: Type.Ref(Markdown.Document).annotations({
18
+ description: 'The ID of the document to write.',
19
+ }),
20
+ content: Schema.String.annotations({
21
+ description: 'New content to write to the document.',
22
+ }),
23
+ }),
24
+ outputSchema: Schema.Void,
25
+ handler: Effect.fn(function* ({ data: { doc, content } }) {
26
+ const document = yield* Database.load(doc);
27
+ const text = yield* Database.load(document.content);
28
+ Obj.change(text, (t) => {
29
+ t.content = content;
30
+ });
31
+ }),
32
+ });
@@ -0,0 +1 @@
1
+ {"conversations":[{"parameters":{"model":"@anthropic/claude-sonnet-4-0","stream":true,"tools":[{"name":"graph_writer","description":"Write to the local graph database","inputSchema":{"type":"object","required":[],"properties":{"objects_dxos_org_type_Organization":{"type":"array","items":{"type":"object","required":["id"],"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"status":{"type":"string","enum":["prospect","qualified","active","commit","reject"],"title":"Status"},"image":{"type":"string","title":"Image"},"website":{"type":"string","title":"Website"},"id":{"type":"string","description":"The id of this object. Come up with a unique id based on your judgement."}},"additionalProperties":false},"description":"The objects of type: dxos.org/type/Organization. An organization."}},"additionalProperties":false}}]},"prompt":{"content":[{"role":"system","content":"Extract the sender's organization from the email. If you are not sure, do nothing.\nThe extracted organization URL must match the sender's email domain.\n,","options":{}},{"role":"user","content":"{\"source\":{\"id\":\"01JGFJJZ00G0WKQSJGMAKCNT87\",\"@type\":\"dxn:type:dxos.org/type/Message:0.2.0\",\"@meta\":{\"keys\":[],\"tags\":[\"important\"]},\"blocks\":[{\"_tag\":\"text\",\"text\":\"Hey team, what's up?\"},{\"_tag\":\"text\",\"text\":\"I'm working on a new algorithm today.\"},{\"_tag\":\"text\",\"text\":\"Anything new from the research team?\"}],\"created\":\"2025-01-01T00:00:00.000Z\",\"sender\":{\"email\":\"john.smith@anthropic.com\",\"name\":\"John Smith\"}},\"contact\":{\"id\":\"01JGFJJZ00G0WKQSJGMAKCNT89\",\"@type\":\"dxn:type:dxos.org/type/Person:0.1.0\",\"@meta\":{\"keys\":[],\"tags\":[\"important\"]},\"emails\":[{\"value\":\"john.smith@anthropic.com\"}],\"fullName\":\"John Smith\"}}","options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_01R1zT7R6NEdWg598pnsyNeA","modelId":"claude-sonnet-4-20250514","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"text-start","id":"0","metadata":{}},{"type":"text-delta","id":"0","delta":"I can","metadata":{}},{"type":"text-delta","id":"0","delta":" see that","metadata":{}},{"type":"text-delta","id":"0","delta":" the","metadata":{}},{"type":"text-delta","id":"0","delta":" sender","metadata":{}},{"type":"text-delta","id":"0","delta":"'s","metadata":{}},{"type":"text-delta","id":"0","delta":" email","metadata":{}},{"type":"text-delta","id":"0","delta":" is from","metadata":{}},{"type":"text-delta","id":"0","delta":" the","metadata":{}},{"type":"text-delta","id":"0","delta":" domain","metadata":{}},{"type":"text-delta","id":"0","delta":" \"anthropic.com\".","metadata":{}},{"type":"text-delta","id":"0","delta":" I","metadata":{}},{"type":"text-delta","id":"0","delta":"'ll extract and","metadata":{}},{"type":"text-delta","id":"0","delta":" create","metadata":{}},{"type":"text-delta","id":"0","delta":" an","metadata":{}},{"type":"text-delta","id":"0","delta":" organization record","metadata":{}},{"type":"text-delta","id":"0","delta":" for","metadata":{}},{"type":"text-delta","id":"0","delta":" Anthropic based on this","metadata":{}},{"type":"text-delta","id":"0","delta":" email","metadata":{}},{"type":"text-delta","id":"0","delta":" domain","metadata":{}},{"type":"text-delta","id":"0","delta":".","metadata":{}},{"type":"text-end","id":"0","metadata":{}},{"type":"tool-params-start","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","name":"graph_writer","providerExecuted":false,"metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"{\"obje","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"cts_dxos","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"_org_","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"type_Org","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"aniza","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"tion\"","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":": ","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"[{\"id\":\"an","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"thropic\",","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"\"nam","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"e\":\"Anthropi","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"c\",","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"\"we","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"bsite\":","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"\"https://a","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"nthrop","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"ic.","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"com\",\"descri","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"ption\":","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"\"A","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"I sa","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"fet","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"y compan","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"y focuse","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"d on d","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"evelop","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"ing s","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"afe, b","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"eneficia","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"l ","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"AI s","metadata":{}},{"type":"tool-params-delta","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","delta":"ystems\"}]}","metadata":{}},{"type":"tool-params-end","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","metadata":{}},{"type":"tool-call","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","name":"graph_writer","params":{"objects_dxos_org_type_Organization":[{"name":"Anthropic","description":"AI safety company focused on developing safe, beneficial AI systems","website":"https://anthropic.com","id":"anthropic"}]},"providerExecuted":false,"metadata":{}},{"type":"finish","reason":"tool-calls","usage":{"inputTokens":853,"outputTokens":154,"totalTokens":1007},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"input_tokens":853,"output_tokens":2,"server_tool_use":null,"service_tier":"standard"}}}}]},{"parameters":{"model":"@anthropic/claude-sonnet-4-0","stream":true,"tools":[{"name":"graph_writer","description":"Write to the local graph database","inputSchema":{"type":"object","required":[],"properties":{"objects_dxos_org_type_Organization":{"type":"array","items":{"type":"object","required":["id"],"properties":{"name":{"type":"string","title":"Name"},"description":{"type":"string","title":"Description"},"status":{"type":"string","enum":["prospect","qualified","active","commit","reject"],"title":"Status"},"image":{"type":"string","title":"Image"},"website":{"type":"string","title":"Website"},"id":{"type":"string","description":"The id of this object. Come up with a unique id based on your judgement."}},"additionalProperties":false},"description":"The objects of type: dxos.org/type/Organization. An organization."}},"additionalProperties":false}}]},"prompt":{"content":[{"role":"system","content":"Extract the sender's organization from the email. If you are not sure, do nothing.\nThe extracted organization URL must match the sender's email domain.\n,","options":{}},{"role":"user","content":"{\"source\":{\"id\":\"01JGFJJZ00G0WKQSJGMAKCNT87\",\"@type\":\"dxn:type:dxos.org/type/Message:0.2.0\",\"@meta\":{\"keys\":[],\"tags\":[\"important\"]},\"blocks\":[{\"_tag\":\"text\",\"text\":\"Hey team, what's up?\"},{\"_tag\":\"text\",\"text\":\"I'm working on a new algorithm today.\"},{\"_tag\":\"text\",\"text\":\"Anything new from the research team?\"}],\"created\":\"2025-01-01T00:00:00.000Z\",\"sender\":{\"email\":\"john.smith@anthropic.com\",\"name\":\"John Smith\"}},\"contact\":{\"id\":\"01JGFJJZ00G0WKQSJGMAKCNT89\",\"@type\":\"dxn:type:dxos.org/type/Person:0.1.0\",\"@meta\":{\"keys\":[],\"tags\":[\"important\"]},\"emails\":[{\"value\":\"john.smith@anthropic.com\"}],\"fullName\":\"John Smith\"}}","options":{}},{"role":"assistant","content":[{"type":"text","text":"I can see that the sender's email is from the domain \"anthropic.com\". I'll extract and create an organization record for Anthropic based on this email domain.","options":{}},{"type":"tool-call","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","name":"graph_writer","params":{"objects_dxos_org_type_Organization":[{"id":"anthropic","name":"Anthropic","website":"https://anthropic.com","description":"AI safety company focused on developing safe, beneficial AI systems"}]},"providerExecuted":false,"options":{}}],"options":{}},{"role":"tool","content":[{"type":"tool-result","id":"toolu_01BMGcZUghBKE9Zj3zs7b2NL","name":"graph_writer","isFailure":false,"result":["dxn:queue:data:BDJXMULPW5FB55UZ7KP5HLDHVTXUTHE2U:01JGFJJZ00G0WKQSJGMAKCNT8B:01JGFJJZ00G0WKQSJGMAKCNT8G"],"providerExecuted":false,"options":{}}],"options":{"anthropic":{"cacheControl":{"ttl":"5m","type":"ephemeral"}}}}]},"response":[{"type":"response-metadata","id":"msg_01EfBN9rt7NxPo6mGTuhB8s2","modelId":"claude-sonnet-4-20250514","timestamp":"1970-01-01T00:00:00.000Z","metadata":{}},{"type":"text-start","id":"0","metadata":{}},{"type":"text-delta","id":"0","delta":"I","metadata":{}},{"type":"text-delta","id":"0","delta":"'ve successfully extracted and","metadata":{}},{"type":"text-delta","id":"0","delta":" created an","metadata":{}},{"type":"text-delta","id":"0","delta":" organization record for Anthropic based","metadata":{}},{"type":"text-delta","id":"0","delta":" on the sender's email domain (","metadata":{}},{"type":"text-delta","id":"0","delta":"anthropic.com). The organization has","metadata":{}},{"type":"text-delta","id":"0","delta":" been added","metadata":{}},{"type":"text-delta","id":"0","delta":" to the graph","metadata":{}},{"type":"text-delta","id":"0","delta":" database","metadata":{}},{"type":"text-delta","id":"0","delta":" with the name","metadata":{}},{"type":"text-delta","id":"0","delta":" \"Anthropic\" and their","metadata":{}},{"type":"text-delta","id":"0","delta":" corresponding","metadata":{}},{"type":"text-delta","id":"0","delta":" website.","metadata":{}},{"type":"text-end","id":"0","metadata":{}},{"type":"finish","reason":"stop","usage":{"inputTokens":6,"outputTokens":51,"totalTokens":57},"metadata":{"anthropic":{"usage":{"cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"cache_creation_input_tokens":0,"cache_read_input_tokens":1083,"input_tokens":6,"output_tokens":1,"server_tool_use":null,"service_tier":"standard"}}}}]}]}
@@ -0,0 +1,74 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, it } from '@effect/vitest';
6
+ import * as Effect from 'effect/Effect';
7
+
8
+ import { MemoizedAiService } from '@dxos/ai/testing';
9
+ import { AssistantTestLayer } from '@dxos/assistant/testing';
10
+ import { Blueprint } from '@dxos/blueprints';
11
+ import { Obj } from '@dxos/echo';
12
+ import { Database } from '@dxos/echo';
13
+ import { TestHelpers } from '@dxos/effect/testing';
14
+ import { FunctionInvocationService } from '@dxos/functions';
15
+ import { ObjectId } from '@dxos/keys';
16
+ import { Message, Organization, Person } from '@dxos/types';
17
+
18
+ import { ResearchGraph } from '../research';
19
+
20
+ import { default as entityExtraction } from './entity-extraction';
21
+
22
+ ObjectId.dangerouslyDisableRandomness();
23
+
24
+ const TestLayer = AssistantTestLayer({
25
+ functions: [entityExtraction],
26
+ types: [Blueprint.Blueprint, Message.Message, Person.Person, Organization.Organization, ResearchGraph],
27
+ });
28
+
29
+ describe('Entity extraction', () => {
30
+ it.effect(
31
+ 'call a function to generate a research report',
32
+ Effect.fnUntraced(
33
+ function* (_) {
34
+ const email = yield* Database.add(
35
+ Obj.make(Message.Message, {
36
+ [Obj.Meta]: {
37
+ tags: ['important'],
38
+ },
39
+ created: new Date('2025-01-01').toISOString(),
40
+ sender: {
41
+ name: 'John Smith',
42
+ email: 'john.smith@anthropic.com',
43
+ },
44
+ blocks: [
45
+ {
46
+ _tag: 'text',
47
+ text: "Hey team, what's up?",
48
+ },
49
+ {
50
+ _tag: 'text',
51
+ text: "I'm working on a new algorithm today.",
52
+ },
53
+ {
54
+ _tag: 'text',
55
+ text: 'Anything new from the research team?',
56
+ },
57
+ ],
58
+ }),
59
+ );
60
+ yield* Database.flush({ indexes: true });
61
+ const result = yield* FunctionInvocationService.invokeFunction(entityExtraction, {
62
+ source: email,
63
+ });
64
+ expect(result.entities).toHaveLength(2);
65
+ for (const entity of result.entities ?? []) {
66
+ expect(Obj.getMeta(entity)?.tags).toContain('important');
67
+ }
68
+ },
69
+ Effect.provide(TestLayer),
70
+ TestHelpers.provideTestContext,
71
+ ),
72
+ MemoizedAiService.isGenerationEnabled() ? 60_000 : undefined,
73
+ );
74
+ });