reflex-agent 0.2.4 → 0.3.1

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 (206) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +111 -98
  3. package/.next/app-path-routes-manifest.json +9 -9
  4. package/.next/build-manifest.json +5 -5
  5. package/.next/prerender-manifest.json +4 -54
  6. package/.next/react-loadable-manifest.json +1 -1
  7. package/.next/server/app/_not-found/page.js +1 -1
  8. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  9. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  10. package/.next/server/app/agents/[agentId]/page.js +3 -3
  11. package/.next/server/app/agents/[agentId]/page.js.nft.json +1 -1
  12. package/.next/server/app/agents/[agentId]/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/agents/[agentId]/respond/route.js +2 -2
  14. package/.next/server/app/api/agents/[agentId]/respond/route.js.nft.json +1 -1
  15. package/.next/server/app/api/agents/[agentId]/respond/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/images/[rootId]/[file]/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/oauth/callback/route.js +3 -3
  18. package/.next/server/app/api/oauth/callback/route_client-reference-manifest.js +1 -1
  19. package/.next/server/app/api/oauth/start/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/roots/[id]/attachments/route.js +0 -0
  21. package/.next/server/app/api/roots/[id]/attachments/route_client-reference-manifest.js +1 -1
  22. package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route.js +2 -2
  23. package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route.js.nft.json +1 -1
  24. package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route.js +2 -2
  26. package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route.js.nft.json +1 -1
  27. package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route_client-reference-manifest.js +1 -1
  28. package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route.js +2 -2
  29. package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route.js.nft.json +1 -1
  30. package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/roots/[id]/dashboard/route.js +1 -1
  32. package/.next/server/app/api/roots/[id]/dashboard/route.js.nft.json +1 -1
  33. package/.next/server/app/api/roots/[id]/dashboard/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/roots/[id]/suggestions/route.js +1 -1
  35. package/.next/server/app/api/roots/[id]/suggestions/route.js.nft.json +1 -1
  36. package/.next/server/app/api/roots/[id]/suggestions/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/utilities/[scope]/[id]/bundle.js/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/utilities/[scope]/[id]/host/route.js +2 -2
  39. package/.next/server/app/api/utilities/[scope]/[id]/host/route.js.nft.json +1 -1
  40. package/.next/server/app/api/utilities/[scope]/[id]/host/route_client-reference-manifest.js +1 -1
  41. package/.next/server/app/api/utilities/[scope]/[id]/host-api.mjs/route_client-reference-manifest.js +1 -1
  42. package/.next/server/app/api/utilities/[scope]/[id]/host-ui.mjs/route_client-reference-manifest.js +1 -1
  43. package/.next/server/app/api/utilities/[scope]/[id]/iframe/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/utilities/[scope]/[id]/style.css/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/audit/page.js +2 -2
  46. package/.next/server/app/audit/page.js.nft.json +1 -1
  47. package/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/onboarding/page.js +4 -4
  49. package/.next/server/app/onboarding/page.js.nft.json +1 -1
  50. package/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  51. package/.next/server/app/page.js +2 -2
  52. package/.next/server/app/page.js.nft.json +1 -1
  53. package/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/server/app/roots/[id]/chat/[topicId]/page.js +2 -6
  55. package/.next/server/app/roots/[id]/chat/[topicId]/page.js.nft.json +1 -1
  56. package/.next/server/app/roots/[id]/chat/[topicId]/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/roots/[id]/kb/[...slug]/page.js +2 -6
  58. package/.next/server/app/roots/[id]/kb/[...slug]/page.js.nft.json +1 -1
  59. package/.next/server/app/roots/[id]/kb/[...slug]/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app/roots/[id]/page.js +3 -3
  61. package/.next/server/app/roots/[id]/page.js.nft.json +1 -1
  62. package/.next/server/app/roots/[id]/page_client-reference-manifest.js +1 -1
  63. package/.next/server/app/roots/[id]/workflows/[wfId]/page.js +2 -2
  64. package/.next/server/app/roots/[id]/workflows/[wfId]/page.js.nft.json +1 -1
  65. package/.next/server/app/roots/[id]/workflows/[wfId]/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/roots/[id]/workflows/page.js +2 -2
  67. package/.next/server/app/roots/[id]/workflows/page.js.nft.json +1 -1
  68. package/.next/server/app/roots/[id]/workflows/page_client-reference-manifest.js +1 -1
  69. package/.next/server/app/roots/new/page.js +4 -2
  70. package/.next/server/app/roots/new/page.js.nft.json +1 -1
  71. package/.next/server/app/roots/new/page_client-reference-manifest.js +1 -1
  72. package/.next/server/app/settings/page.js +6 -6
  73. package/.next/server/app/settings/page.js.nft.json +1 -1
  74. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/share/[id]/file/page.js +2 -2
  76. package/.next/server/app/share/[id]/file/page.js.nft.json +1 -1
  77. package/.next/server/app/share/[id]/file/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app/share/[id]/page.js +2 -2
  79. package/.next/server/app/share/[id]/page.js.nft.json +1 -1
  80. package/.next/server/app/share/[id]/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app/utilities/[scope]/[id]/page.js +2 -2
  82. package/.next/server/app/utilities/[scope]/[id]/page.js.nft.json +1 -1
  83. package/.next/server/app/utilities/[scope]/[id]/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/utilities/page.js +2 -17
  85. package/.next/server/app/utilities/page.js.nft.json +1 -1
  86. package/.next/server/app/utilities/page_client-reference-manifest.js +1 -1
  87. package/.next/server/app-paths-manifest.json +9 -9
  88. package/.next/server/chunks/1223.js +1 -1
  89. package/.next/server/chunks/133.js +1 -490
  90. package/.next/server/chunks/1888.js +1 -1
  91. package/.next/server/chunks/{9739.js → 1988.js} +13 -9
  92. package/.next/server/chunks/2433.js +1 -1
  93. package/.next/server/chunks/2503.js +1 -1
  94. package/.next/server/chunks/285.js +471 -0
  95. package/.next/server/chunks/2959.js +1 -0
  96. package/.next/server/chunks/2995.js +1 -0
  97. package/.next/server/chunks/3240.js +1 -1
  98. package/.next/server/chunks/3332.js +1 -1
  99. package/.next/server/chunks/3657.js +1 -1
  100. package/.next/server/chunks/4066.js +1 -1
  101. package/.next/server/chunks/4438.js +1 -0
  102. package/.next/server/chunks/4514.js +3 -0
  103. package/.next/server/chunks/4553.js +1 -1
  104. package/.next/server/chunks/4812.js +179 -0
  105. package/.next/server/chunks/4925.js +1 -1
  106. package/.next/server/chunks/{3953.js → 5068.js} +2 -2
  107. package/.next/server/chunks/5319.js +1 -1
  108. package/.next/server/chunks/569.js +1 -1
  109. package/.next/server/chunks/6730.js +1 -1
  110. package/.next/server/chunks/6909.js +142 -161
  111. package/.next/server/chunks/8262.js +1 -1
  112. package/.next/server/chunks/9098.js +1 -1
  113. package/.next/server/chunks/94.js +1 -1
  114. package/.next/server/chunks/9427.js +1 -0
  115. package/.next/server/chunks/9538.js +1 -0
  116. package/.next/server/chunks/963.js +1 -0
  117. package/.next/server/chunks/9835.js +1 -1
  118. package/.next/server/middleware-build-manifest.js +1 -1
  119. package/.next/server/middleware-manifest.json +5 -5
  120. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  121. package/.next/server/pages/500.html +1 -1
  122. package/.next/server/pages-manifest.json +1 -2
  123. package/.next/server/server-reference-manifest.js +1 -1
  124. package/.next/server/server-reference-manifest.json +1 -1
  125. package/.next/static/chunks/2865-134f546f21ca4330.js +1 -0
  126. package/.next/static/chunks/4108.5abdb7812a13eafd.js +1 -0
  127. package/.next/static/chunks/5254-4196f25e56270de5.js +1 -0
  128. package/.next/static/chunks/5521-cbc665104c7e59d3.js +1 -0
  129. package/.next/static/chunks/6445-99824866a51b582a.js +1 -0
  130. package/.next/static/chunks/8855-9b941d2b78f398ce.js +1 -0
  131. package/.next/static/chunks/8871-2948840b33c0863d.js +1 -0
  132. package/.next/static/chunks/9411-af5f758c57741929.js +3 -0
  133. package/.next/static/chunks/app/agents/[agentId]/page-5d6f4cb16b42d02b.js +1 -0
  134. package/.next/static/chunks/app/layout-d4cf24375db6d793.js +1 -0
  135. package/.next/static/chunks/app/onboarding/page-7303664b62ccc24a.js +1 -0
  136. package/.next/static/chunks/app/page-97d312db91d569f7.js +1 -0
  137. package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-123f60a544619a3c.js +1 -0
  138. package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-e253597edb1b2440.js +1 -0
  139. package/.next/static/chunks/app/roots/[id]/page-91a8de6a1c79f8a3.js +1 -0
  140. package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-4300a52e163883df.js +1 -0
  141. package/.next/static/chunks/app/roots/new/page-6b104aad46a38173.js +1 -0
  142. package/.next/static/chunks/app/settings/page-afe1b80f7f45c5eb.js +1 -0
  143. package/.next/static/chunks/app/share/[id]/page-a5fb565bd892d4df.js +1 -0
  144. package/.next/static/chunks/app/utilities/[scope]/[id]/page-eb713a2b5209942c.js +1 -0
  145. package/.next/static/chunks/app/utilities/page-b7f30c151c42a27c.js +1 -0
  146. package/.next/static/chunks/{webpack-5fca180586957874.js → webpack-bddc3babcbc30dd7.js} +1 -1
  147. package/.next/static/css/60e9b6cdf1283e83.css +1 -0
  148. package/.next/trace +47 -46
  149. package/dist/lib/reflex/agents/prompts.js +46 -46
  150. package/dist/lib/reflex/agents/prompts.js.map +1 -1
  151. package/dist/lib/reflex/prompts/defaults.js +102 -102
  152. package/next.config.ts +4 -1
  153. package/package.json +2 -2
  154. package/.next/server/app/_not-found.html +0 -1
  155. package/.next/server/app/_not-found.meta +0 -8
  156. package/.next/server/app/_not-found.rsc +0 -18
  157. package/.next/server/app/index.html +0 -1
  158. package/.next/server/app/index.meta +0 -9
  159. package/.next/server/app/index.rsc +0 -19
  160. package/.next/server/chunks/1410.js +0 -1
  161. package/.next/server/chunks/1986.js +0 -1
  162. package/.next/server/chunks/2448.js +0 -3
  163. package/.next/server/chunks/5754.js +0 -3
  164. package/.next/server/chunks/7097.js +0 -1
  165. package/.next/server/chunks/7782.js +0 -1
  166. package/.next/server/chunks/7987.js +0 -1
  167. package/.next/server/chunks/810.js +0 -1
  168. package/.next/server/chunks/8843.js +0 -1
  169. package/.next/server/chunks/9328.js +0 -179
  170. package/.next/server/pages/404.html +0 -1
  171. package/.next/static/chunks/2488-c9590facb4b9f184.js +0 -1
  172. package/.next/static/chunks/2684-257d38989ef53935.js +0 -1
  173. package/.next/static/chunks/4108.fb9f99a9c899ef54.js +0 -1
  174. package/.next/static/chunks/6231-d83c1544bbea8424.js +0 -1
  175. package/.next/static/chunks/9045-731ff0865352dd95.js +0 -1
  176. package/.next/static/chunks/9496-75ccd3fadb294fba.js +0 -1
  177. package/.next/static/chunks/992-4e7b7f722c629e21.js +0 -1
  178. package/.next/static/chunks/app/agents/[agentId]/page-0b5c2838354d0eba.js +0 -1
  179. package/.next/static/chunks/app/layout-9a59ed07c18cb786.js +0 -1
  180. package/.next/static/chunks/app/onboarding/page-79f07a813ea2abfe.js +0 -1
  181. package/.next/static/chunks/app/page-27f4b98b02ac4f79.js +0 -1
  182. package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-8db2d0b75cd333c8.js +0 -1
  183. package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-873b131eec3a2f30.js +0 -1
  184. package/.next/static/chunks/app/roots/[id]/page-270d0d49eb668784.js +0 -1
  185. package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-7c1f10dbe0bcb9ad.js +0 -1
  186. package/.next/static/chunks/app/roots/new/page-ac1a9f6379710ca2.js +0 -1
  187. package/.next/static/chunks/app/settings/page-81cb1393e817dfc3.js +0 -1
  188. package/.next/static/chunks/app/share/[id]/page-2d123f0a99e1606f.js +0 -1
  189. package/.next/static/chunks/app/utilities/[scope]/[id]/page-0bbb8d17af80c1da.js +0 -1
  190. package/.next/static/chunks/app/utilities/page-e6ce673b9357bf1f.js +0 -1
  191. package/.next/static/css/87e01f779d555d04.css +0 -1
  192. package/packages/utilities/learn-anything/README.md +0 -41
  193. package/packages/utilities/learn-anything/actions/_json.ts +0 -191
  194. package/packages/utilities/learn-anything/actions/_store.ts +0 -248
  195. package/packages/utilities/learn-anything/actions/buildModule.ts +0 -487
  196. package/packages/utilities/learn-anything/actions/explainSelection.ts +0 -64
  197. package/packages/utilities/learn-anything/actions/generateOutline.ts +0 -170
  198. package/packages/utilities/learn-anything/actions/generateQuiz.ts +0 -72
  199. package/packages/utilities/learn-anything/actions/generateTrainer.ts +0 -106
  200. package/packages/utilities/learn-anything/actions/refreshCourseCard.ts +0 -76
  201. package/packages/utilities/learn-anything/actions/tutorAsk.ts +0 -93
  202. package/packages/utilities/learn-anything/article-view.tsx +0 -464
  203. package/packages/utilities/learn-anything/manifest.json +0 -42
  204. package/packages/utilities/learn-anything/ui.tsx +0 -1589
  205. /package/.next/static/{og_wC7UPkGtJDiapaTgBr → IGuuMcet1qtGZQCP2MEn4}/_buildManifest.js +0 -0
  206. /package/.next/static/{og_wC7UPkGtJDiapaTgBr → IGuuMcet1qtGZQCP2MEn4}/_ssgManifest.js +0 -0
@@ -1,487 +0,0 @@
1
- import { reflex } from "@host/api";
2
- import { callJsonAgent, snippet } from "./_json";
3
-
4
- /**
5
- * Compile a learning module: agent writes a full markdown article, then
6
- * Reflex resolves the image needs (real photos via search → local
7
- * download, schematics via generation) and embeds permanent local URLs
8
- * into the body. Persisted as a `course-module` KB entry, idempotent on
9
- * (courseId, moduleId).
10
- *
11
- * Why image queries/prompts instead of raw URLs:
12
- * The old contract asked the LLM to suggest image URLs from its
13
- * training corpus. ~90% of those URLs were dead or hallucinated.
14
- * Now the LLM emits *intent* (what to find / what to draw) and
15
- * reflex.images.search + reflex.images.generate do the real work,
16
- * yielding stable `/api/images/<rootId>/<sha>.<ext>` URLs that move
17
- * with the project.
18
- */
19
-
20
- export interface ModuleContent {
21
- courseId: string;
22
- moduleId: string;
23
- title: string;
24
- /** Long-form markdown body for the article view. */
25
- article: string;
26
- videos: Array<{ title: string; url: string; note?: string }>;
27
- links: Array<{ title: string; url: string; snippet?: string }>;
28
- /** Resolved images: each has a permanent `/api/images/...` URL. */
29
- images: Array<{
30
- /** Stable id from the LLM draft; used to substitute inline `[[IMG:<id>]]` placeholders. */
31
- id: string;
32
- alt: string;
33
- url: string;
34
- source: "search" | "generated";
35
- attribution?: { name: string; link: string };
36
- }>;
37
- /** Mermaid diagrams (kept for structural schemas like flowcharts). */
38
- diagrams: Array<{ title: string; mermaid: string }>;
39
- homework: string[];
40
- relPath: string;
41
- }
42
-
43
- export interface BuildModuleArgs {
44
- courseId: string;
45
- moduleId: string;
46
- moduleTitle: string;
47
- moduleObjective: string;
48
- topic: string;
49
- }
50
-
51
- interface LlmDraft {
52
- article?: string;
53
- videos?: Array<{ title: string; url: string; note?: string }>;
54
- links?: Array<{ title: string; url: string; snippet?: string }>;
55
- /**
56
- * "Find a real photo of X". Each carries an `id` so the LLM can drop
57
- * an inline `[[IMG:<id>]]` placeholder into `article` and we know
58
- * where to embed the resolved image.
59
- */
60
- imageQueries?: Array<{ id: string; alt: string; query: string }>;
61
- /** Same id-based contract as `imageQueries`, but the resolver calls
62
- * reflex.images.generate instead of search. */
63
- generatedFigures?: Array<{ id: string; alt: string; prompt: string }>;
64
- /** Mermaid code for diagrams that genuinely need flow/sequence syntax. */
65
- diagrams?: Array<{ title: string; mermaid: string }>;
66
- homework?: string[];
67
- }
68
-
69
- export default async function buildModule(
70
- args: BuildModuleArgs,
71
- ): Promise<ModuleContent> {
72
- // 1. Pull a few web sources to ground the article — saves the agent from
73
- // hallucinating sources. Best-effort; failures are silent.
74
- let webContext = "";
75
- try {
76
- const search = await reflex.web.search({
77
- query: `${args.topic} ${args.moduleTitle}`,
78
- });
79
- const top = (search.results ?? []).slice(0, 4);
80
- webContext = top
81
- .map(
82
- (r, i) =>
83
- `[${i + 1}] ${r.title}\n ${r.url}\n ${r.snippet ?? ""}`,
84
- )
85
- .join("\n");
86
- } catch {
87
- /* offline / no search — agent will rely on training data */
88
- }
89
-
90
- // 2. Ask the agent to draft the module. Note: NO bare image URLs —
91
- // the LLM tells us WHAT to look for / draw; Reflex resolves it via
92
- // reflex.images.search (Brave when available) + reflex.images.generate.
93
- const prompt = [
94
- `Курс: «${args.topic}». Модуль: «${args.moduleTitle}» — ${args.moduleObjective}.`,
95
- "Подготовь учебный материал. Структура JSON-ответа:",
96
- "{",
97
- ` "article": "длинный markdown 800-2000 слов; используй # ## ### и плейсхолдеры [[IMG:<id>]] для inline-картинок",`,
98
- ` "videos": [{"title":"...","url":"https://youtube.com/...","note":"..."}],`,
99
- ` "links": [{"title":"...","url":"...","snippet":"..."}],`,
100
- ` "imageQueries": [{"id":"i1","alt":"...","query":"короткий английский поисковый запрос"}],`,
101
- ` "generatedFigures": [{"id":"f1","alt":"...","prompt":"подробное описание для AI-генератора, английский"}],`,
102
- ` "diagrams": [{"title":"...","mermaid":"graph TD; A-->B;"}],`,
103
- ` "homework": ["...","..."]`,
104
- "}",
105
- "",
106
- "## ВИЗУАЛЬНОЕ СОПРОВОЖДЕНИЕ — ОБЯЗАТЕЛЬНО + INLINE-РАЗМЕЩЕНИЕ",
107
- "",
108
- "Любой учебный модуль ОБЯЗАН быть визуально насыщенным. Каждая картинка ставится **inline** в нужном месте текста через плейсхолдер.",
109
- "",
110
- "### Как это работает",
111
- " 1. Каждому элементу в `imageQueries` и `generatedFigures` присваиваешь ШОРТ-ID (`i1`, `i2`, `f1`, `f2`, ...). i = image-search (real photo), f = figure (AI-generated). Уникальный в пределах модуля.",
112
- " 2. В `article` markdown вставляешь плейсхолдер `[[IMG:i1]]` на ОТДЕЛЬНОЙ СТРОКЕ ровно там где должна стоять эта картинка (например, после абзаца который её обсуждает).",
113
- " 3. Reflex автоматически:",
114
- " – ищет реальные фото/схемы через Brave Image Search (по `imageQueries`),",
115
- " – ВИЗУАЛЬНО ОЦЕНИВАЕТ кандидатов (твой chat-агент смотрит на thumbnails через Read tool) — клипарт/off-topic отклоняются по содержимому,",
116
- " – генерирует уникальные иллюстрации через Gemini Nano Banana (по `generatedFigures`),",
117
- " – заменяет `[[IMG:<id>]]` на `![alt](локальный-url)` с атрибуцией,",
118
- " – неотрезолвленные id (картинка не найдена / отклонена) удаляются из текста чисто.",
119
- "",
120
- "### Пример",
121
- "```markdown",
122
- "## Зрительная кора",
123
- "Зрительная кора V1 — первая зона обработки информации от сетчатки.",
124
- "",
125
- "[[IMG:i1]]",
126
- "",
127
- "Колончатая организация V1 была описана Хьюбелом и Визелом в 1962 году...",
128
- "",
129
- "[[IMG:f1]]",
130
- "```",
131
- "...где `imageQueries: [{id:\"i1\", alt:\"Срез зрительной коры V1\", query:\"primary visual cortex V1 histology\"}]` и `generatedFigures: [{id:\"f1\", alt:\"Схема рецептивного поля\", prompt:\"educational diagram: receptive field of simple cells in V1, ON-OFF regions, labeled in English\"}]`.",
132
- "",
133
- "Твоя задача — заполнить два массива И расставить плейсхолдеры в article:",
134
- "",
135
- "### `imageQueries` (поиск реальных материалов) — МИНИМУМ 2-3 шт.",
136
- " • Для тем где есть реальные референсы (Эйфелева башня, клетка, Гражданская война, лабораторная установка, известная картина, ландшафт, исторический документ) — ВСЕГДА добавляй 2-4 query.",
137
- " • Каждый query — короткий АНГЛИЙСКИЙ поисковый запрос (Brave работает лучше на английском): \"Eiffel Tower iron lattice closeup\", \"mitochondria electron microscope\", \"American Civil War Gettysburg battlefield\".",
138
- " • `id` — короткий уникальный идентификатор: `i1`, `i2`, `i3`...",
139
- " • `alt` — короткое описание по-русски, что зритель увидит.",
140
- " • Каждому `id` соответствует ровно один плейсхолдер `[[IMG:i1]]` в `article` — ставь его в тематически подходящем месте.",
141
- "",
142
- "### `generatedFigures` (AI-генерация уникальных схем) — 1-2 шт когда уместно.",
143
- " • Используй для уникальных схем/иллюстраций, которых нет в сети: \"процесс N в виде наглядной схемы\", \"анатомия Х в стиле учебника\", \"таймлайн событий\", \"абстрактная визуализация концепции\".",
144
- " • `id` — короткий уникальный идентификатор: `f1`, `f2`...",
145
- " • `prompt` — подробный АНГЛИЙСКИЙ описательный prompt со стилем (\"minimalist educational diagram, white background, labeled parts in blue, isometric view\" / \"watercolor illustration, soft palette\" / \"photorealistic, studio lighting\").",
146
- " • НЕ дублируй generatedFigures с imageQueries — generate только то, что не найти готовым.",
147
- " • `alt` — короткое описание по-русски.",
148
- " • Поставь плейсхолдер `[[IMG:f1]]` в article ровно где эта схема нужна.",
149
- "",
150
- "### Правила для прочих полей",
151
- " • article — основной текст, 800-2000 слов. Заголовки # ## ###, плотный материал.",
152
- " • Картинки размещаются ТОЛЬКО через `[[IMG:<id>]]` на отдельной строке. Не пиши `[[ИЛЛЮСТРАЦИЯ: ...]]`, `[[СХЕМА: ...]]` — они не работают.",
153
- " • НЕ ВЫДУМЫВАЙ URL картинок. Любые bare URL в article игнорируются.",
154
- " • Каждый id, объявленный в imageQueries/generatedFigures, должен встретиться в article ровно один раз. Каждый `[[IMG:<id>]]` в тексте должен иметь соответствие в одном из массивов.",
155
- " • videos: 1-3 ссылки на youtube/youtu.be — URL юзер сам проверит.",
156
- " • links: 2-5 авторитетных статей.",
157
- " • diagrams (mermaid): только flowchart/sequence/class — где mermaid реально удобнее картинки. Для visual schemes используй generatedFigures.",
158
- " • homework: 3-5 практических заданий с проверяемым результатом.",
159
- "",
160
- "Верни ТОЛЬКО JSON одной строкой, без markdown-фенс.",
161
- "",
162
- webContext
163
- ? `## Web-источники для опоры\n${webContext}`
164
- : "## Web-источники недоступны — опирайся на свои знания.",
165
- ].join("\n");
166
-
167
- const result = await callJsonAgent<LlmDraft>({
168
- prompt,
169
- invoke: (p) => reflex.agent.invoke({ prompt: p, timeoutMs: 7 * 60_000 }),
170
- maxAttempts: 4,
171
- shapeHint:
172
- `{"article":"...","videos":[],"links":[],"imageQueries":[],"generatedFigures":[],"diagrams":[],"homework":[]}\n` +
173
- `article — markdown 800-2000 слов. Все массивы — обязательно массивы (можно пустые).`,
174
- validate: (p) => {
175
- const v = p as LlmDraft;
176
- return typeof v?.article === "string" && v.article.trim().length > 40
177
- ? v
178
- : null;
179
- },
180
- });
181
- if (!result.ok) {
182
- throw new Error(
183
- `Не удалось собрать модуль за ${result.attempts} попыток (${result.reason}). ` +
184
- `Последний ответ: «${snippet(result.lastText, 200)}».`,
185
- );
186
- }
187
- const draft = result.value;
188
-
189
- // 3. Resolve image needs in parallel. Each call is best-effort —
190
- // a failed search or generation just drops that image from the module
191
- // (we don't want one flaky API to fail the whole build).
192
- const [searchedImages, generatedImages] = await Promise.all([
193
- resolveSearches(draft.imageQueries ?? [], args.topic, args.moduleTitle),
194
- resolveGenerations(draft.generatedFigures ?? []),
195
- ]);
196
-
197
- // Inline-place images by substituting [[IMG:<id>]] placeholders in the
198
- // article body. Any image whose id wasn't referenced (LLM forgot)
199
- // falls back into the trailing Иллюстрации section so nothing gets
200
- // dropped silently. Residual unknown [[...]] markers are stripped.
201
- const allImages = [...searchedImages, ...generatedImages];
202
- const { article: articleWithImages, placedIds } = substituteImagePlaceholders(
203
- typeof draft.article === "string" ? draft.article : "",
204
- allImages,
205
- );
206
- const articleClean = stripPlaceholderMarkers(articleWithImages);
207
- const unplaced = allImages.filter((im) => !placedIds.has(im.id));
208
-
209
- const content: Omit<ModuleContent, "relPath"> = {
210
- courseId: args.courseId,
211
- moduleId: args.moduleId,
212
- title: args.moduleTitle,
213
- article: articleClean,
214
- videos: sanitizeArr(draft.videos, ["title", "url"]) as ModuleContent["videos"],
215
- links: sanitizeArr(draft.links, ["title", "url"]) as ModuleContent["links"],
216
- images: allImages,
217
- diagrams: sanitizeArr(draft.diagrams, ["title", "mermaid"]) as ModuleContent["diagrams"],
218
- homework: Array.isArray(draft.homework)
219
- ? draft.homework.map(String).filter(Boolean).slice(0, 8)
220
- : [],
221
- };
222
-
223
- // 4. Persist as a KB entry. Frontmatter holds the structured bits;
224
- // body = article with images already inlined + optional fallback
225
- // section for images the LLM declared but never referenced.
226
- const body = [
227
- content.article,
228
- unplaced.length > 0
229
- ? "\n\n## Дополнительные иллюстрации\n" +
230
- unplaced
231
- .map((im) => `${renderInlineImage(im)}`)
232
- .join("\n\n")
233
- : "",
234
- content.diagrams.length > 0
235
- ? "\n\n## Схемы\n" +
236
- content.diagrams
237
- .map(
238
- (d) =>
239
- `### ${d.title}\n\n\`\`\`mermaid\n${d.mermaid}\n\`\`\``,
240
- )
241
- .join("\n\n")
242
- : "",
243
- ]
244
- .filter(Boolean)
245
- .join("");
246
-
247
- const saved = await reflex.kb.add({
248
- kind: "course-module",
249
- title: `${args.topic} · ${args.moduleTitle}`,
250
- body,
251
- meta: {
252
- courseId: args.courseId,
253
- moduleId: args.moduleId,
254
- videos: JSON.stringify(content.videos),
255
- links: JSON.stringify(content.links),
256
- images: JSON.stringify(content.images),
257
- diagrams: JSON.stringify(content.diagrams),
258
- homework: JSON.stringify(content.homework),
259
- title: args.moduleTitle,
260
- objective: args.moduleObjective,
261
- },
262
- slug: `${args.courseId}-${args.moduleId}`,
263
- });
264
-
265
- await reflex.audit.log({
266
- type: "module-built",
267
- payload: {
268
- courseId: args.courseId,
269
- moduleId: args.moduleId,
270
- videos: content.videos.length,
271
- images: content.images.length,
272
- searched: searchedImages.length,
273
- generated: generatedImages.length,
274
- },
275
- });
276
-
277
- return { ...content, relPath: saved.relPath };
278
- }
279
-
280
- /**
281
- * For each query: do a web image search (5 candidates), ask the LLM
282
- * which best fits the course material, attach the chosen one. If the
283
- * LLM rejects all candidates (-1), the image slot is left empty — better
284
- * than embedding clip-art or off-topic stock photo. Try/catch isolates
285
- * per-image failures from killing the module build.
286
- */
287
- async function resolveSearches(
288
- queries: Array<{ id?: unknown; alt?: unknown; query?: unknown }>,
289
- topic: string,
290
- moduleTitle: string,
291
- ): Promise<ModuleContent["images"]> {
292
- const clean = queries
293
- .filter(
294
- (q) =>
295
- q &&
296
- typeof q.query === "string" &&
297
- q.query.trim().length > 0,
298
- )
299
- .slice(0, 4);
300
- const out = await Promise.all(
301
- clean.map(async (q, idx) => {
302
- try {
303
- const query = q.query as string;
304
- const alt = typeof q.alt === "string" ? q.alt : query;
305
- const id =
306
- typeof q.id === "string" && q.id.trim().length > 0
307
- ? q.id.trim()
308
- : `i${idx + 1}`;
309
- // Default provider: Brave for breadth (real web), falls back to
310
- // Unsplash/Pexels via service-router based on which key exists.
311
- const search = await reflex.images.search({ query, count: 5 });
312
- if (search.results.length === 0) return null;
313
- // Vision-based pick: Reflex spawns the user's chat harness
314
- // (Codex / Claude Code) on the candidate thumbnails and asks it
315
- // to choose. The agent uses its Read tool — both runtimes get
316
- // real vision content for image files, so off-topic results
317
- // (clipart, mislabelled photos) are filtered by content, not
318
- // metadata.
319
- const pick = await reflex.images.pickBest({
320
- query,
321
- alt,
322
- context: `${topic} → ${moduleTitle}`,
323
- candidates: search.results.map((r) => ({
324
- url: r.url,
325
- thumb: r.thumb,
326
- attribution: r.attribution,
327
- })),
328
- });
329
- if (pick.pickIndex < 0 || pick.pickIndex >= search.results.length) {
330
- void reflex.audit.log({
331
- type: "image-rejected",
332
- payload: { query, reason: pick.reason, via: pick.via },
333
- });
334
- return null;
335
- }
336
- const chosen = search.results[pick.pickIndex];
337
- const attached = await reflex.images.attach({
338
- sourceUrl: chosen.url,
339
- });
340
- return {
341
- id,
342
- alt,
343
- url: attached.url,
344
- source: "search" as const,
345
- attribution: chosen.attribution,
346
- };
347
- } catch (err) {
348
- // Log to audit; the module survives without this image.
349
- void reflex.audit.log({
350
- type: "image-search-failed",
351
- payload: {
352
- query: q.query,
353
- error: err instanceof Error ? err.message : String(err),
354
- },
355
- });
356
- return null;
357
- }
358
- }),
359
- );
360
- return out.filter((x): x is NonNullable<typeof x> => x !== null);
361
- }
362
-
363
- /**
364
- * Walk through the article body, find `[[IMG:<id>]]` placeholders, and
365
- * replace each with a markdown image reference using the resolved
366
- * image's URL + alt + attribution.
367
- *
368
- * Returns the rewritten article + the set of ids successfully placed.
369
- * Unknown placeholders (id has no matching resolved image) get stripped
370
- * — likely a generation/search failure that already logged to audit.
371
- *
372
- * Whitespace around inline placeholders is normalized so the image sits
373
- * as its own block (Markdown then renders `![](...)` as a paragraph).
374
- */
375
- function substituteImagePlaceholders(
376
- article: string,
377
- images: ModuleContent["images"],
378
- ): { article: string; placedIds: Set<string> } {
379
- const byId = new Map(images.map((im) => [im.id, im]));
380
- const placedIds = new Set<string>();
381
- // Accept variants: `[[IMG:i1]]`, `[[img:i1]]`, `[[IMG: i1 ]]`,
382
- // `[[IMG i1]]`, and even single-bracket `[IMG:i1]` from sloppy LLMs.
383
- const re = /\[\[?\s*IMG\s*[:\s]\s*([A-Za-z0-9_-]+)\s*\]\]?/gi;
384
- const replaced = article.replace(re, (_, rawId: string) => {
385
- const id = rawId.trim();
386
- const img = byId.get(id);
387
- if (!img) return "";
388
- placedIds.add(id);
389
- return `\n\n${renderInlineImage(img)}\n\n`;
390
- });
391
- return { article: replaced, placedIds };
392
- }
393
-
394
- function renderInlineImage(im: ModuleContent["images"][number]): string {
395
- const safeAlt = im.alt.replace(/[\[\]\n]/g, " ").slice(0, 200);
396
- const credit =
397
- im.source === "search" && im.attribution?.name
398
- ? `\n\n_Источник: [${im.attribution.name}](${im.attribution.link || im.url})_`
399
- : im.source === "generated"
400
- ? `\n\n_Сгенерировано AI_`
401
- : "";
402
- return `![${safeAlt}](${im.url})${credit}`;
403
- }
404
-
405
- /**
406
- * Strip residual `[[ИЛЛЮСТРАЦИЯ: ...]]` / `[[СХЕМА: ...]]` / orphan
407
- * `[[IMG:<unknown-id>]]` placeholders the LLM might emit despite the
408
- * prompt forbidding them OR after `substituteImagePlaceholders` failed
409
- * to find the id. Drops the marker, collapses surrounding whitespace.
410
- *
411
- * Matches Russian variants + Latin spellings for safety. Case-insensitive.
412
- */
413
- function stripPlaceholderMarkers(text: string): string {
414
- const re =
415
- /\[\[?\s*(?:ИЛЛЮСТРАЦИЯ|СХЕМА|ILLUSTRATION|SCHEME|IMAGE|IMG|DIAGRAM)\b[^\]]*?\]\]?/giu;
416
- let out = text.replace(re, "");
417
- // Clean up double blank lines + trailing whitespace the removal may
418
- // have left behind.
419
- out = out.replace(/[ \t]+\n/g, "\n");
420
- out = out.replace(/\n{3,}/g, "\n\n");
421
- return out.trim() + "\n";
422
- }
423
-
424
- async function resolveGenerations(
425
- figures: Array<{ id?: unknown; alt?: unknown; prompt?: unknown }>,
426
- ): Promise<ModuleContent["images"]> {
427
- const clean = figures
428
- .filter(
429
- (f) =>
430
- f &&
431
- typeof f.prompt === "string" &&
432
- f.prompt.trim().length > 0,
433
- )
434
- .slice(0, 2);
435
- const out = await Promise.all(
436
- clean.map(async (f, idx) => {
437
- try {
438
- const gen = await reflex.images.generate({
439
- prompt: f.prompt as string,
440
- aspectRatio: "16:9",
441
- });
442
- const id =
443
- typeof f.id === "string" && f.id.trim().length > 0
444
- ? f.id.trim()
445
- : `f${idx + 1}`;
446
- return {
447
- id,
448
- alt: typeof f.alt === "string" ? f.alt : (f.prompt as string).slice(0, 80),
449
- url: gen.url,
450
- source: "generated" as const,
451
- };
452
- } catch (err) {
453
- void reflex.audit.log({
454
- type: "image-generate-failed",
455
- payload: {
456
- prompt: f.prompt,
457
- error: err instanceof Error ? err.message : String(err),
458
- },
459
- });
460
- return null;
461
- }
462
- }),
463
- );
464
- return out.filter((x): x is NonNullable<typeof x> => x !== null);
465
- }
466
-
467
- function sanitizeArr(
468
- v: unknown,
469
- required: string[],
470
- ): Array<Record<string, string>> {
471
- if (!Array.isArray(v)) return [];
472
- const out: Array<Record<string, string>> = [];
473
- for (const item of v.slice(0, 12)) {
474
- if (typeof item !== "object" || item === null) continue;
475
- const o = item as Record<string, unknown>;
476
- const ok = required.every(
477
- (k) => typeof o[k] === "string" && (o[k] as string).trim() !== "",
478
- );
479
- if (!ok) continue;
480
- const row: Record<string, string> = {};
481
- for (const [k, val] of Object.entries(o)) {
482
- if (typeof val === "string") row[k] = val;
483
- }
484
- out.push(row);
485
- }
486
- return out;
487
- }
@@ -1,64 +0,0 @@
1
- import { reflex } from "@host/api";
2
-
3
- /**
4
- * "Explain this" feature: user highlights a snippet inside the module
5
- * article and asks for a deeper explanation. The agent gets the
6
- * surrounding paragraph as context so it understands what "это" means.
7
- *
8
- * Two modes:
9
- * 1. Default — `question` omitted, agent gives a generic 2-5 paragraph
10
- * breakdown of the selected fragment.
11
- * 2. Custom — `question` supplied (book-style margin annotation), agent
12
- * answers that specific question with the selection as the focal
13
- * point. Lets the reader say things like "при чём тут N?" or
14
- * "дай пример" instead of always getting the same boilerplate.
15
- */
16
-
17
- export interface ExplainSelectionArgs {
18
- selection: string;
19
- /** ~400-1000 chars around the selection for context. */
20
- context: string;
21
- topic: string;
22
- moduleTitle: string;
23
- /** Optional user question about the selection. */
24
- question?: string;
25
- }
26
-
27
- export default async function explainSelection(
28
- args: ExplainSelectionArgs,
29
- ): Promise<{ text: string }> {
30
- const userQuestion = (args.question ?? "").trim();
31
- const promptLines: string[] = [
32
- `Курс: «${args.topic}». Модуль: «${args.moduleTitle}».`,
33
- ];
34
-
35
- if (userQuestion) {
36
- promptLines.push(
37
- "Пользователь выделил фрагмент и задал конкретный вопрос про этот фрагмент.",
38
- "Ответь именно на его вопрос, опираясь на выделение + окружающий контекст. 2-4 абзаца, без воды, по делу.",
39
- "Markdown без заголовков; короткие фразы, пример если уместен.",
40
- );
41
- } else {
42
- promptLines.push(
43
- "Пользователь выделил фрагмент и просит объяснить подробнее.",
44
- "Дай развёрнутое объяснение в 2-5 абзацах: что это значит, как работает,",
45
- "почему именно так, конкретный пример. Без воды. Markdown без заголовков.",
46
- );
47
- }
48
-
49
- promptLines.push(
50
- "",
51
- `## Окружающий контекст\n${args.context.slice(0, 1500)}`,
52
- "",
53
- `## Выделение пользователя\n«${args.selection.slice(0, 800)}»`,
54
- );
55
- if (userQuestion) {
56
- promptLines.push("", `## Вопрос пользователя\n${userQuestion.slice(0, 600)}`);
57
- }
58
-
59
- const r = await reflex.llm.complete({
60
- task: "quick",
61
- prompt: promptLines.join("\n"),
62
- });
63
- return { text: (r.text ?? "").trim() };
64
- }