reflex-agent 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +83 -83
- package/.next/app-path-routes-manifest.json +6 -6
- package/.next/build-manifest.json +5 -5
- package/.next/prerender-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/server/app/_not-found/page.js +2 -2
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/agents/[agentId]/page.js +2 -2
- package/.next/server/app/agents/[agentId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/agents/[agentId]/respond/route.js +1 -1
- package/.next/server/app/api/agents/[agentId]/respond/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/images/[rootId]/[file]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/oauth/callback/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/oauth/start/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/attachments/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/send/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stop/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route.js +2 -2
- package/.next/server/app/api/roots/[id]/chat/[topicId]/stream/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/dashboard/route.js +1 -1
- package/.next/server/app/api/roots/[id]/dashboard/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/roots/[id]/suggestions/route.js +1 -1
- package/.next/server/app/api/roots/[id]/suggestions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/bundle.js/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/host/route.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/host/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/host-api.mjs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/host-ui.mjs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/iframe/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/utilities/[scope]/[id]/style.css/route_client-reference-manifest.js +1 -1
- package/.next/server/app/audit/page.js +1 -1
- package/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/server/app/onboarding/page.js +3 -3
- package/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/chat/[topicId]/page.js +2 -2
- package/.next/server/app/roots/[id]/chat/[topicId]/page.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/chat/[topicId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/kb/[...slug]/page.js +2 -2
- package/.next/server/app/roots/[id]/kb/[...slug]/page.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/kb/[...slug]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/page.js +3 -3
- package/.next/server/app/roots/[id]/page.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/workflows/[wfId]/page.js +2 -2
- package/.next/server/app/roots/[id]/workflows/[wfId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/[id]/workflows/page.js +1 -1
- package/.next/server/app/roots/[id]/workflows/page.js.nft.json +1 -1
- package/.next/server/app/roots/[id]/workflows/page_client-reference-manifest.js +1 -1
- package/.next/server/app/roots/new/page.js +4 -4
- package/.next/server/app/roots/new/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/page.js +4 -4
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/share/[id]/file/page.js +2 -2
- package/.next/server/app/share/[id]/file/page_client-reference-manifest.js +1 -1
- package/.next/server/app/share/[id]/page.js +2 -2
- package/.next/server/app/share/[id]/page.js.nft.json +1 -1
- package/.next/server/app/share/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/utilities/[scope]/[id]/page.js +2 -2
- package/.next/server/app/utilities/[scope]/[id]/page.js.nft.json +1 -1
- package/.next/server/app/utilities/[scope]/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/utilities/page.js +2 -2
- package/.next/server/app/utilities/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +6 -6
- package/.next/server/chunks/157.js +1 -0
- package/.next/server/chunks/285.js +2 -2
- package/.next/server/chunks/2995.js +1 -1
- package/.next/server/chunks/3332.js +1 -1
- package/.next/server/chunks/4812.js +1 -1
- package/.next/server/chunks/4925.js +1 -1
- package/.next/server/chunks/503.js +1 -0
- package/.next/server/chunks/5992.js +1 -0
- package/.next/server/chunks/6307.js +1 -0
- package/.next/server/chunks/{3512.js → 7908.js} +2 -2
- package/.next/server/chunks/8587.js +3 -0
- package/.next/server/chunks/9098.js +1 -1
- package/.next/server/functions-config-manifest.json +4 -4
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/1117-6fde4a3e0fe0443a.js +1 -0
- package/.next/static/chunks/1479-1d103a2aa91aa824.js +1 -0
- package/.next/static/chunks/1592-67762f29bd458262.js +1 -0
- package/.next/static/chunks/2052-31a610cee521cd24.js +1 -0
- package/.next/static/chunks/4108.e6f31641845cd071.js +1 -0
- package/.next/static/chunks/8322-71eceae9d2259389.js +1 -0
- package/.next/static/chunks/app/layout-b254fa87e3c3025e.js +1 -0
- package/.next/static/chunks/app/onboarding/page-a540d2a334e61279.js +1 -0
- package/.next/static/chunks/app/page-6127cec5577bbdea.js +1 -0
- package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-ce22f38ff1971ce7.js +1 -0
- package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-520df769ed074d58.js +1 -0
- package/.next/static/chunks/app/roots/[id]/page-667fac103c710695.js +1 -0
- package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-520200e8c167cb0f.js +1 -0
- package/.next/static/chunks/app/roots/new/page-72b4e06184ec3712.js +1 -0
- package/.next/static/chunks/app/settings/page-6a441614d14c707d.js +1 -0
- package/.next/static/chunks/app/share/[id]/page-10997d1668345672.js +1 -0
- package/.next/static/chunks/app/utilities/[scope]/[id]/page-4a33cee7cb9a7bf7.js +1 -0
- package/.next/static/chunks/app/utilities/page-df07d2ec05d7743a.js +1 -0
- package/.next/static/chunks/{webpack-2b0eab4ccdf44f63.js → webpack-ff7ea73bc08ce1d7.js} +1 -1
- package/.next/static/css/60e9b6cdf1283e83.css +1 -0
- package/.next/trace +47 -47
- package/package.json +1 -2
- package/.next/server/chunks/1.js +0 -3
- package/.next/server/chunks/2192.js +0 -1
- package/.next/server/chunks/6734.js +0 -1
- package/.next/server/chunks/7215.js +0 -1
- package/.next/server/chunks/9944.js +0 -1
- package/.next/static/chunks/1082-326e649fb24d4945.js +0 -1
- package/.next/static/chunks/3736-f4e42d6d38be50b0.js +0 -1
- package/.next/static/chunks/4108.ca0bdf3cbf3c56cc.js +0 -1
- package/.next/static/chunks/7482-7ef26030a10ce14f.js +0 -1
- package/.next/static/chunks/8944-c4f2406ecd61094f.js +0 -1
- package/.next/static/chunks/9415-eb6b5d4c2de3a7c0.js +0 -1
- package/.next/static/chunks/app/layout-85eb1fd21dab0895.js +0 -1
- package/.next/static/chunks/app/onboarding/page-2013bd8124b9162e.js +0 -1
- package/.next/static/chunks/app/page-558a224e13ffb52c.js +0 -1
- package/.next/static/chunks/app/roots/[id]/chat/[topicId]/page-b42f03fd58669d12.js +0 -1
- package/.next/static/chunks/app/roots/[id]/kb/[...slug]/page-7d17b4e6a5231f56.js +0 -1
- package/.next/static/chunks/app/roots/[id]/page-4aab5266f432e37e.js +0 -1
- package/.next/static/chunks/app/roots/[id]/workflows/[wfId]/page-1ee3320bf5744efc.js +0 -1
- package/.next/static/chunks/app/roots/new/page-df8d2c1f0c64c37a.js +0 -1
- package/.next/static/chunks/app/settings/page-fdba798d9e243ad3.js +0 -1
- package/.next/static/chunks/app/share/[id]/page-818a451d05e08d26.js +0 -1
- package/.next/static/chunks/app/utilities/[scope]/[id]/page-2cee09cc2ab9b5e8.js +0 -1
- package/.next/static/chunks/app/utilities/page-44a51522b347f13e.js +0 -1
- package/.next/static/css/4b367c1d0fa99b78.css +0 -1
- package/packages/utilities/learn-anything/README.md +0 -41
- package/packages/utilities/learn-anything/actions/_json.ts +0 -191
- package/packages/utilities/learn-anything/actions/_store.ts +0 -248
- package/packages/utilities/learn-anything/actions/buildModule.ts +0 -488
- package/packages/utilities/learn-anything/actions/explainSelection.ts +0 -65
- package/packages/utilities/learn-anything/actions/generateOutline.ts +0 -170
- package/packages/utilities/learn-anything/actions/generateQuiz.ts +0 -72
- package/packages/utilities/learn-anything/actions/generateTrainer.ts +0 -106
- package/packages/utilities/learn-anything/actions/refreshCourseCard.ts +0 -76
- package/packages/utilities/learn-anything/actions/tutorAsk.ts +0 -93
- package/packages/utilities/learn-anything/article-view.tsx +0 -464
- package/packages/utilities/learn-anything/manifest.json +0 -42
- package/packages/utilities/learn-anything/ui.tsx +0 -1589
- /package/.next/static/{fhVNqfmJl5Mdfhyhg6orp → z5pbSy6TRHko5NGqhD4cn}/_buildManifest.js +0 -0
- /package/.next/static/{fhVNqfmJl5Mdfhyhg6orp → z5pbSy6TRHko5NGqhD4cn}/_ssgManifest.js +0 -0
|
@@ -1,488 +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
|
-
`Course: "${args.topic}". Module: "${args.moduleTitle}" — ${args.moduleObjective}.`,
|
|
95
|
-
"Produce the learning material. JSON reply shape:",
|
|
96
|
-
"{",
|
|
97
|
-
` "article": "long markdown 800-2000 words; use # ## ### and [[IMG:<id>]] placeholders for inline images",`,
|
|
98
|
-
` "videos": [{"title":"...","url":"https://youtube.com/...","note":"..."}],`,
|
|
99
|
-
` "links": [{"title":"...","url":"...","snippet":"..."}],`,
|
|
100
|
-
` "imageQueries": [{"id":"i1","alt":"...","query":"short English search query"}],`,
|
|
101
|
-
` "generatedFigures": [{"id":"f1","alt":"...","prompt":"detailed English description for the AI generator"}],`,
|
|
102
|
-
` "diagrams": [{"title":"...","mermaid":"graph TD; A-->B;"}],`,
|
|
103
|
-
` "homework": ["...","..."]`,
|
|
104
|
-
"}",
|
|
105
|
-
"",
|
|
106
|
-
"## VISUAL CONTENT — MANDATORY + INLINE PLACEMENT",
|
|
107
|
-
"",
|
|
108
|
-
"Every learning module MUST be visually rich. Each image is placed **inline** at the right spot in the text via a placeholder.",
|
|
109
|
-
"",
|
|
110
|
-
"### How it works",
|
|
111
|
-
" 1. Each entry in `imageQueries` and `generatedFigures` gets a SHORT ID (`i1`, `i2`, `f1`, `f2`, ...). i = image-search (real photo), f = figure (AI-generated). Unique within the module.",
|
|
112
|
-
" 2. In the `article` markdown insert the placeholder `[[IMG:i1]]` on its OWN LINE exactly where the image should appear (e.g. after the paragraph that discusses it).",
|
|
113
|
-
" 3. Reflex automatically:",
|
|
114
|
-
" – searches for real photos/diagrams via Brave Image Search (using `imageQueries`),",
|
|
115
|
-
" – VISUALLY EVALUATES candidates (your chat agent inspects thumbnails via the Read tool) — clipart / off-topic items are rejected based on content,",
|
|
116
|
-
" – generates unique illustrations via Gemini Nano Banana (using `generatedFigures`),",
|
|
117
|
-
" – replaces `[[IMG:<id>]]` with `` plus attribution,",
|
|
118
|
-
" – unresolved ids (image not found / rejected) are cleanly stripped from the text.",
|
|
119
|
-
"",
|
|
120
|
-
"### Example",
|
|
121
|
-
"```markdown",
|
|
122
|
-
"## Visual cortex",
|
|
123
|
-
"The primary visual cortex V1 is the first area that processes information from the retina.",
|
|
124
|
-
"",
|
|
125
|
-
"[[IMG:i1]]",
|
|
126
|
-
"",
|
|
127
|
-
"The columnar organisation of V1 was described by Hubel and Wiesel in 1962...",
|
|
128
|
-
"",
|
|
129
|
-
"[[IMG:f1]]",
|
|
130
|
-
"```",
|
|
131
|
-
"...where `imageQueries: [{id:\"i1\", alt:\"Section of the primary visual cortex V1\", query:\"primary visual cortex V1 histology\"}]` and `generatedFigures: [{id:\"f1\", alt:\"Receptive field diagram\", prompt:\"educational diagram: receptive field of simple cells in V1, ON-OFF regions, labeled in English\"}]`.",
|
|
132
|
-
"",
|
|
133
|
-
"Your job is to fill both arrays AND place the placeholders in `article`:",
|
|
134
|
-
"",
|
|
135
|
-
"### `imageQueries` (real-image search) — AT LEAST 2-3 items.",
|
|
136
|
-
" • For topics with real-world references (Eiffel Tower, a cell, the Civil War, lab equipment, a famous painting, a landscape, a historical document) — ALWAYS add 2-4 queries.",
|
|
137
|
-
" • Each `query` is a short ENGLISH search string (Brave works best in English): \"Eiffel Tower iron lattice closeup\", \"mitochondria electron microscope\", \"American Civil War Gettysburg battlefield\".",
|
|
138
|
-
" • `id` — short unique identifier: `i1`, `i2`, `i3`...",
|
|
139
|
-
" • `alt` — short description of what the viewer will see.",
|
|
140
|
-
" • Each `id` corresponds to exactly one `[[IMG:i1]]` placeholder in `article` — put it in a thematically appropriate spot.",
|
|
141
|
-
"",
|
|
142
|
-
"### `generatedFigures` (AI-generated unique figures) — 1-2 items when appropriate.",
|
|
143
|
-
" • Use for unique schemes/illustrations that aren't available online: \"process N as a clear schematic\", \"anatomy of X in textbook style\", \"event timeline\", \"abstract visualisation of a concept\".",
|
|
144
|
-
" • `id` — short unique identifier: `f1`, `f2`...",
|
|
145
|
-
" • `prompt` — detailed ENGLISH descriptive prompt including style (\"minimalist educational diagram, white background, labeled parts in blue, isometric view\" / \"watercolor illustration, soft palette\" / \"photorealistic, studio lighting\").",
|
|
146
|
-
" • DO NOT duplicate generatedFigures with imageQueries — only generate what cannot be found ready-made.",
|
|
147
|
-
" • `alt` — short description.",
|
|
148
|
-
" • Place the `[[IMG:f1]]` placeholder in `article` exactly where the figure is needed.",
|
|
149
|
-
"",
|
|
150
|
-
"### Rules for the other fields",
|
|
151
|
-
" • article — main text, 800-2000 words. Headings # ## ###, dense content.",
|
|
152
|
-
" • Images are placed ONLY via `[[IMG:<id>]]` on its own line. Do not write `[[ILLUSTRATION: ...]]`, `[[DIAGRAM: ...]]` — they don't work.",
|
|
153
|
-
" • DO NOT MAKE UP image URLs. Any bare URL in `article` is ignored.",
|
|
154
|
-
" • Every id declared in imageQueries/generatedFigures must appear in `article` exactly once. Every `[[IMG:<id>]]` in the text must have a matching entry in one of the arrays.",
|
|
155
|
-
" • videos: 1-3 youtube/youtu.be links — the user verifies URLs themselves.",
|
|
156
|
-
" • links: 2-5 authoritative articles.",
|
|
157
|
-
" • diagrams (mermaid): only flowchart/sequence/class — where mermaid is genuinely more convenient than an image. For visual schemes use generatedFigures.",
|
|
158
|
-
" • homework: 3-5 practical exercises with a verifiable result.",
|
|
159
|
-
"",
|
|
160
|
-
"Reply with JSON ONLY on a single line, no markdown fences.",
|
|
161
|
-
"",
|
|
162
|
-
webContext
|
|
163
|
-
? `## Web sources to ground on\n${webContext}`
|
|
164
|
-
: "## Web sources unavailable — rely on your own knowledge.",
|
|
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 words. Every list field must be an array (empty is OK).`,
|
|
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
|
-
`Failed to build the module in ${result.attempts} attempts (${result.reason}). ` +
|
|
184
|
-
`Last reply: "${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 "Additional illustrations" section so
|
|
200
|
-
// nothing gets dropped silently. Residual unknown [[...]] markers are
|
|
201
|
-
// stripped.
|
|
202
|
-
const allImages = [...searchedImages, ...generatedImages];
|
|
203
|
-
const { article: articleWithImages, placedIds } = substituteImagePlaceholders(
|
|
204
|
-
typeof draft.article === "string" ? draft.article : "",
|
|
205
|
-
allImages,
|
|
206
|
-
);
|
|
207
|
-
const articleClean = stripPlaceholderMarkers(articleWithImages);
|
|
208
|
-
const unplaced = allImages.filter((im) => !placedIds.has(im.id));
|
|
209
|
-
|
|
210
|
-
const content: Omit<ModuleContent, "relPath"> = {
|
|
211
|
-
courseId: args.courseId,
|
|
212
|
-
moduleId: args.moduleId,
|
|
213
|
-
title: args.moduleTitle,
|
|
214
|
-
article: articleClean,
|
|
215
|
-
videos: sanitizeArr(draft.videos, ["title", "url"]) as ModuleContent["videos"],
|
|
216
|
-
links: sanitizeArr(draft.links, ["title", "url"]) as ModuleContent["links"],
|
|
217
|
-
images: allImages,
|
|
218
|
-
diagrams: sanitizeArr(draft.diagrams, ["title", "mermaid"]) as ModuleContent["diagrams"],
|
|
219
|
-
homework: Array.isArray(draft.homework)
|
|
220
|
-
? draft.homework.map(String).filter(Boolean).slice(0, 8)
|
|
221
|
-
: [],
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// 4. Persist as a KB entry. Frontmatter holds the structured bits;
|
|
225
|
-
// body = article with images already inlined + optional fallback
|
|
226
|
-
// section for images the LLM declared but never referenced.
|
|
227
|
-
const body = [
|
|
228
|
-
content.article,
|
|
229
|
-
unplaced.length > 0
|
|
230
|
-
? "\n\n## Additional illustrations\n" +
|
|
231
|
-
unplaced
|
|
232
|
-
.map((im) => `${renderInlineImage(im)}`)
|
|
233
|
-
.join("\n\n")
|
|
234
|
-
: "",
|
|
235
|
-
content.diagrams.length > 0
|
|
236
|
-
? "\n\n## Diagrams\n" +
|
|
237
|
-
content.diagrams
|
|
238
|
-
.map(
|
|
239
|
-
(d) =>
|
|
240
|
-
`### ${d.title}\n\n\`\`\`mermaid\n${d.mermaid}\n\`\`\``,
|
|
241
|
-
)
|
|
242
|
-
.join("\n\n")
|
|
243
|
-
: "",
|
|
244
|
-
]
|
|
245
|
-
.filter(Boolean)
|
|
246
|
-
.join("");
|
|
247
|
-
|
|
248
|
-
const saved = await reflex.kb.add({
|
|
249
|
-
kind: "course-module",
|
|
250
|
-
title: `${args.topic} · ${args.moduleTitle}`,
|
|
251
|
-
body,
|
|
252
|
-
meta: {
|
|
253
|
-
courseId: args.courseId,
|
|
254
|
-
moduleId: args.moduleId,
|
|
255
|
-
videos: JSON.stringify(content.videos),
|
|
256
|
-
links: JSON.stringify(content.links),
|
|
257
|
-
images: JSON.stringify(content.images),
|
|
258
|
-
diagrams: JSON.stringify(content.diagrams),
|
|
259
|
-
homework: JSON.stringify(content.homework),
|
|
260
|
-
title: args.moduleTitle,
|
|
261
|
-
objective: args.moduleObjective,
|
|
262
|
-
},
|
|
263
|
-
slug: `${args.courseId}-${args.moduleId}`,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
await reflex.audit.log({
|
|
267
|
-
type: "module-built",
|
|
268
|
-
payload: {
|
|
269
|
-
courseId: args.courseId,
|
|
270
|
-
moduleId: args.moduleId,
|
|
271
|
-
videos: content.videos.length,
|
|
272
|
-
images: content.images.length,
|
|
273
|
-
searched: searchedImages.length,
|
|
274
|
-
generated: generatedImages.length,
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
return { ...content, relPath: saved.relPath };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* For each query: do a web image search (5 candidates), ask the LLM
|
|
283
|
-
* which best fits the course material, attach the chosen one. If the
|
|
284
|
-
* LLM rejects all candidates (-1), the image slot is left empty — better
|
|
285
|
-
* than embedding clip-art or off-topic stock photo. Try/catch isolates
|
|
286
|
-
* per-image failures from killing the module build.
|
|
287
|
-
*/
|
|
288
|
-
async function resolveSearches(
|
|
289
|
-
queries: Array<{ id?: unknown; alt?: unknown; query?: unknown }>,
|
|
290
|
-
topic: string,
|
|
291
|
-
moduleTitle: string,
|
|
292
|
-
): Promise<ModuleContent["images"]> {
|
|
293
|
-
const clean = queries
|
|
294
|
-
.filter(
|
|
295
|
-
(q) =>
|
|
296
|
-
q &&
|
|
297
|
-
typeof q.query === "string" &&
|
|
298
|
-
q.query.trim().length > 0,
|
|
299
|
-
)
|
|
300
|
-
.slice(0, 4);
|
|
301
|
-
const out = await Promise.all(
|
|
302
|
-
clean.map(async (q, idx) => {
|
|
303
|
-
try {
|
|
304
|
-
const query = q.query as string;
|
|
305
|
-
const alt = typeof q.alt === "string" ? q.alt : query;
|
|
306
|
-
const id =
|
|
307
|
-
typeof q.id === "string" && q.id.trim().length > 0
|
|
308
|
-
? q.id.trim()
|
|
309
|
-
: `i${idx + 1}`;
|
|
310
|
-
// Default provider: Brave for breadth (real web), falls back to
|
|
311
|
-
// Unsplash/Pexels via service-router based on which key exists.
|
|
312
|
-
const search = await reflex.images.search({ query, count: 5 });
|
|
313
|
-
if (search.results.length === 0) return null;
|
|
314
|
-
// Vision-based pick: Reflex spawns the user's chat harness
|
|
315
|
-
// (Codex / Claude Code) on the candidate thumbnails and asks it
|
|
316
|
-
// to choose. The agent uses its Read tool — both runtimes get
|
|
317
|
-
// real vision content for image files, so off-topic results
|
|
318
|
-
// (clipart, mislabelled photos) are filtered by content, not
|
|
319
|
-
// metadata.
|
|
320
|
-
const pick = await reflex.images.pickBest({
|
|
321
|
-
query,
|
|
322
|
-
alt,
|
|
323
|
-
context: `${topic} → ${moduleTitle}`,
|
|
324
|
-
candidates: search.results.map((r) => ({
|
|
325
|
-
url: r.url,
|
|
326
|
-
thumb: r.thumb,
|
|
327
|
-
attribution: r.attribution,
|
|
328
|
-
})),
|
|
329
|
-
});
|
|
330
|
-
if (pick.pickIndex < 0 || pick.pickIndex >= search.results.length) {
|
|
331
|
-
void reflex.audit.log({
|
|
332
|
-
type: "image-rejected",
|
|
333
|
-
payload: { query, reason: pick.reason, via: pick.via },
|
|
334
|
-
});
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
const chosen = search.results[pick.pickIndex];
|
|
338
|
-
const attached = await reflex.images.attach({
|
|
339
|
-
sourceUrl: chosen.url,
|
|
340
|
-
});
|
|
341
|
-
return {
|
|
342
|
-
id,
|
|
343
|
-
alt,
|
|
344
|
-
url: attached.url,
|
|
345
|
-
source: "search" as const,
|
|
346
|
-
attribution: chosen.attribution,
|
|
347
|
-
};
|
|
348
|
-
} catch (err) {
|
|
349
|
-
// Log to audit; the module survives without this image.
|
|
350
|
-
void reflex.audit.log({
|
|
351
|
-
type: "image-search-failed",
|
|
352
|
-
payload: {
|
|
353
|
-
query: q.query,
|
|
354
|
-
error: err instanceof Error ? err.message : String(err),
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
}),
|
|
360
|
-
);
|
|
361
|
-
return out.filter((x): x is NonNullable<typeof x> => x !== null);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Walk through the article body, find `[[IMG:<id>]]` placeholders, and
|
|
366
|
-
* replace each with a markdown image reference using the resolved
|
|
367
|
-
* image's URL + alt + attribution.
|
|
368
|
-
*
|
|
369
|
-
* Returns the rewritten article + the set of ids successfully placed.
|
|
370
|
-
* Unknown placeholders (id has no matching resolved image) get stripped
|
|
371
|
-
* — likely a generation/search failure that already logged to audit.
|
|
372
|
-
*
|
|
373
|
-
* Whitespace around inline placeholders is normalized so the image sits
|
|
374
|
-
* as its own block (Markdown then renders `` as a paragraph).
|
|
375
|
-
*/
|
|
376
|
-
function substituteImagePlaceholders(
|
|
377
|
-
article: string,
|
|
378
|
-
images: ModuleContent["images"],
|
|
379
|
-
): { article: string; placedIds: Set<string> } {
|
|
380
|
-
const byId = new Map(images.map((im) => [im.id, im]));
|
|
381
|
-
const placedIds = new Set<string>();
|
|
382
|
-
// Accept variants: `[[IMG:i1]]`, `[[img:i1]]`, `[[IMG: i1 ]]`,
|
|
383
|
-
// `[[IMG i1]]`, and even single-bracket `[IMG:i1]` from sloppy LLMs.
|
|
384
|
-
const re = /\[\[?\s*IMG\s*[:\s]\s*([A-Za-z0-9_-]+)\s*\]\]?/gi;
|
|
385
|
-
const replaced = article.replace(re, (_, rawId: string) => {
|
|
386
|
-
const id = rawId.trim();
|
|
387
|
-
const img = byId.get(id);
|
|
388
|
-
if (!img) return "";
|
|
389
|
-
placedIds.add(id);
|
|
390
|
-
return `\n\n${renderInlineImage(img)}\n\n`;
|
|
391
|
-
});
|
|
392
|
-
return { article: replaced, placedIds };
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function renderInlineImage(im: ModuleContent["images"][number]): string {
|
|
396
|
-
const safeAlt = im.alt.replace(/[\[\]\n]/g, " ").slice(0, 200);
|
|
397
|
-
const credit =
|
|
398
|
-
im.source === "search" && im.attribution?.name
|
|
399
|
-
? `\n\n_Source: [${im.attribution.name}](${im.attribution.link || im.url})_`
|
|
400
|
-
: im.source === "generated"
|
|
401
|
-
? `\n\n_Generated by AI_`
|
|
402
|
-
: "";
|
|
403
|
-
return `${credit}`;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Strip residual `[[ILLUSTRATION: ...]]` / `[[DIAGRAM: ...]]` / orphan
|
|
408
|
-
* `[[IMG:<unknown-id>]]` placeholders the LLM might emit despite the
|
|
409
|
-
* prompt forbidding them OR after `substituteImagePlaceholders` failed
|
|
410
|
-
* to find the id. Drops the marker, collapses surrounding whitespace.
|
|
411
|
-
*
|
|
412
|
-
* Matches several spelling variants for safety. Case-insensitive.
|
|
413
|
-
*/
|
|
414
|
-
function stripPlaceholderMarkers(text: string): string {
|
|
415
|
-
const re =
|
|
416
|
-
/\[\[?\s*(?:ILLUSTRATION|SCHEME|IMAGE|IMG|DIAGRAM)\b[^\]]*?\]\]?/giu;
|
|
417
|
-
let out = text.replace(re, "");
|
|
418
|
-
// Clean up double blank lines + trailing whitespace the removal may
|
|
419
|
-
// have left behind.
|
|
420
|
-
out = out.replace(/[ \t]+\n/g, "\n");
|
|
421
|
-
out = out.replace(/\n{3,}/g, "\n\n");
|
|
422
|
-
return out.trim() + "\n";
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
async function resolveGenerations(
|
|
426
|
-
figures: Array<{ id?: unknown; alt?: unknown; prompt?: unknown }>,
|
|
427
|
-
): Promise<ModuleContent["images"]> {
|
|
428
|
-
const clean = figures
|
|
429
|
-
.filter(
|
|
430
|
-
(f) =>
|
|
431
|
-
f &&
|
|
432
|
-
typeof f.prompt === "string" &&
|
|
433
|
-
f.prompt.trim().length > 0,
|
|
434
|
-
)
|
|
435
|
-
.slice(0, 2);
|
|
436
|
-
const out = await Promise.all(
|
|
437
|
-
clean.map(async (f, idx) => {
|
|
438
|
-
try {
|
|
439
|
-
const gen = await reflex.images.generate({
|
|
440
|
-
prompt: f.prompt as string,
|
|
441
|
-
aspectRatio: "16:9",
|
|
442
|
-
});
|
|
443
|
-
const id =
|
|
444
|
-
typeof f.id === "string" && f.id.trim().length > 0
|
|
445
|
-
? f.id.trim()
|
|
446
|
-
: `f${idx + 1}`;
|
|
447
|
-
return {
|
|
448
|
-
id,
|
|
449
|
-
alt: typeof f.alt === "string" ? f.alt : (f.prompt as string).slice(0, 80),
|
|
450
|
-
url: gen.url,
|
|
451
|
-
source: "generated" as const,
|
|
452
|
-
};
|
|
453
|
-
} catch (err) {
|
|
454
|
-
void reflex.audit.log({
|
|
455
|
-
type: "image-generate-failed",
|
|
456
|
-
payload: {
|
|
457
|
-
prompt: f.prompt,
|
|
458
|
-
error: err instanceof Error ? err.message : String(err),
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
}),
|
|
464
|
-
);
|
|
465
|
-
return out.filter((x): x is NonNullable<typeof x> => x !== null);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function sanitizeArr(
|
|
469
|
-
v: unknown,
|
|
470
|
-
required: string[],
|
|
471
|
-
): Array<Record<string, string>> {
|
|
472
|
-
if (!Array.isArray(v)) return [];
|
|
473
|
-
const out: Array<Record<string, string>> = [];
|
|
474
|
-
for (const item of v.slice(0, 12)) {
|
|
475
|
-
if (typeof item !== "object" || item === null) continue;
|
|
476
|
-
const o = item as Record<string, unknown>;
|
|
477
|
-
const ok = required.every(
|
|
478
|
-
(k) => typeof o[k] === "string" && (o[k] as string).trim() !== "",
|
|
479
|
-
);
|
|
480
|
-
if (!ok) continue;
|
|
481
|
-
const row: Record<string, string> = {};
|
|
482
|
-
for (const [k, val] of Object.entries(o)) {
|
|
483
|
-
if (typeof val === "string") row[k] = val;
|
|
484
|
-
}
|
|
485
|
-
out.push(row);
|
|
486
|
-
}
|
|
487
|
-
return out;
|
|
488
|
-
}
|
|
@@ -1,65 +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 "this" 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 "what does N have to do
|
|
14
|
-
* with this?" or "give me an example" instead of always getting the
|
|
15
|
-
* same boilerplate.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export interface ExplainSelectionArgs {
|
|
19
|
-
selection: string;
|
|
20
|
-
/** ~400-1000 chars around the selection for context. */
|
|
21
|
-
context: string;
|
|
22
|
-
topic: string;
|
|
23
|
-
moduleTitle: string;
|
|
24
|
-
/** Optional user question about the selection. */
|
|
25
|
-
question?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export default async function explainSelection(
|
|
29
|
-
args: ExplainSelectionArgs,
|
|
30
|
-
): Promise<{ text: string }> {
|
|
31
|
-
const userQuestion = (args.question ?? "").trim();
|
|
32
|
-
const promptLines: string[] = [
|
|
33
|
-
`Course: "${args.topic}". Module: "${args.moduleTitle}".`,
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
if (userQuestion) {
|
|
37
|
-
promptLines.push(
|
|
38
|
-
"The user highlighted a fragment and asked a specific question about that fragment.",
|
|
39
|
-
"Answer exactly their question, grounded in the selection + surrounding context. 2-4 paragraphs, no fluff, to the point.",
|
|
40
|
-
"Markdown without headings; short sentences, with an example when appropriate.",
|
|
41
|
-
);
|
|
42
|
-
} else {
|
|
43
|
-
promptLines.push(
|
|
44
|
-
"The user highlighted a fragment and is asking for a deeper explanation.",
|
|
45
|
-
"Give a thorough explanation in 2-5 paragraphs: what it means, how it works,",
|
|
46
|
-
"why it works that way, with a concrete example. No fluff. Markdown without headings.",
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
promptLines.push(
|
|
51
|
-
"",
|
|
52
|
-
`## Surrounding context\n${args.context.slice(0, 1500)}`,
|
|
53
|
-
"",
|
|
54
|
-
`## User selection\n"${args.selection.slice(0, 800)}"`,
|
|
55
|
-
);
|
|
56
|
-
if (userQuestion) {
|
|
57
|
-
promptLines.push("", `## User question\n${userQuestion.slice(0, 600)}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const r = await reflex.llm.complete({
|
|
61
|
-
task: "quick",
|
|
62
|
-
prompt: promptLines.join("\n"),
|
|
63
|
-
});
|
|
64
|
-
return { text: (r.text ?? "").trim() };
|
|
65
|
-
}
|