@manta-eu/horns-client 0.1.3 → 0.2.0

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.
@@ -0,0 +1,241 @@
1
+ import type { Horn } from "@manta-eu/horns";
2
+ import { type StudioStep } from "../studio-progress";
3
+ export type StudioConversationState = {
4
+ draft: Horn | null;
5
+ };
6
+ export type StudioTurn = {
7
+ reply: string;
8
+ horn: Horn | undefined;
9
+ };
10
+ /** One Studio conversation. The draft is the object's and arrives through state sync. */
11
+ export type StudioConversationArgs = {
12
+ datastoreId: string;
13
+ conversation: string;
14
+ /** Draws the Horn as it stands and says what is wrong with it. The page is the only thing that can. */
15
+ look: (why: string) => Promise<string>;
16
+ };
17
+ export declare function useStudioConversation(args: StudioConversationArgs): {
18
+ ask: (said: string) => Promise<StudioTurn>;
19
+ open: (horn: Horn) => Promise<void>;
20
+ steps: readonly StudioStep[];
21
+ draft: {
22
+ iri: string;
23
+ slug: string;
24
+ title: string;
25
+ datastoreId: string;
26
+ realm: "hiremanta-com" | "journey-travel";
27
+ schemaVersion: 1;
28
+ revision: number;
29
+ layout: {
30
+ kind: "grid";
31
+ columns: 12;
32
+ items: {
33
+ viewId: string;
34
+ x: number;
35
+ y: number;
36
+ w: number;
37
+ h: number;
38
+ }[];
39
+ };
40
+ variables: {
41
+ id: string;
42
+ label: string;
43
+ type: "string" | "number" | "date" | "iri" | "enum" | "dateRange";
44
+ default?: import("zod").JSONType | undefined;
45
+ options?: {
46
+ values: import("zod").JSONType[];
47
+ } | {
48
+ binding: {
49
+ kind: "select";
50
+ sparql: string;
51
+ params: string[];
52
+ limit: number;
53
+ derivations: string[];
54
+ columns: {
55
+ name: string;
56
+ type: "string" | "number" | "boolean" | "date" | "iri";
57
+ }[];
58
+ } | {
59
+ kind: "lens";
60
+ classIri: string;
61
+ limit: number;
62
+ derivations: string[];
63
+ fields: {
64
+ name: string;
65
+ path: string;
66
+ type: "string" | "number" | "boolean" | "date" | "iri";
67
+ }[];
68
+ rows?: "value" | "entity" | undefined;
69
+ } | {
70
+ kind: "query";
71
+ query: {
72
+ item: string;
73
+ revision: string;
74
+ hash: string;
75
+ };
76
+ params: string[];
77
+ limit: number;
78
+ };
79
+ } | undefined;
80
+ urlParam?: string | undefined;
81
+ }[];
82
+ views: ({
83
+ id: string;
84
+ title: string;
85
+ binding: {
86
+ kind: "select";
87
+ sparql: string;
88
+ params: string[];
89
+ limit: number;
90
+ derivations: string[];
91
+ columns: {
92
+ name: string;
93
+ type: "string" | "number" | "boolean" | "date" | "iri";
94
+ }[];
95
+ } | {
96
+ kind: "lens";
97
+ classIri: string;
98
+ limit: number;
99
+ derivations: string[];
100
+ fields: {
101
+ name: string;
102
+ path: string;
103
+ type: "string" | "number" | "boolean" | "date" | "iri";
104
+ }[];
105
+ rows?: "value" | "entity" | undefined;
106
+ } | {
107
+ kind: "query";
108
+ query: {
109
+ item: string;
110
+ revision: string;
111
+ hash: string;
112
+ };
113
+ params: string[];
114
+ limit: number;
115
+ };
116
+ render: {
117
+ kind: "component";
118
+ component: {
119
+ item: string;
120
+ revision: string;
121
+ hash: string;
122
+ };
123
+ props: import("zod").JSONType;
124
+ } | {
125
+ kind: "tsx";
126
+ source: string;
127
+ reviewed?: {
128
+ hash: string;
129
+ at: string;
130
+ } | undefined;
131
+ };
132
+ emits?: {
133
+ event: string;
134
+ setsVariable: string;
135
+ }[] | undefined;
136
+ } | {
137
+ id: string;
138
+ ref: {
139
+ item: string;
140
+ revision: string;
141
+ hash: string;
142
+ };
143
+ })[];
144
+ derivations: ({
145
+ id: string;
146
+ predicateIri: string;
147
+ appliesTo: string;
148
+ datatype: string;
149
+ pattern: string;
150
+ status: "query" | "staged" | "materialized";
151
+ promotedRuleIri?: string | undefined;
152
+ } | {
153
+ id: string;
154
+ ref: {
155
+ item: string;
156
+ revision: string;
157
+ hash: string;
158
+ };
159
+ })[];
160
+ actions: ({
161
+ kind: "kg-update";
162
+ insert: string;
163
+ allowedPredicates: string[];
164
+ allowedClasses: string[];
165
+ id: string;
166
+ label: string;
167
+ params: {
168
+ id: string;
169
+ type: "string" | "number" | "date" | "iri" | "enum";
170
+ required: boolean;
171
+ classIri?: string | undefined;
172
+ values?: import("zod").JSONType[] | undefined;
173
+ }[];
174
+ confirm: true;
175
+ delete?: string | undefined;
176
+ where?: string | undefined;
177
+ criteria?: {
178
+ ask: string;
179
+ } | undefined;
180
+ } | {
181
+ kind: "action-shape";
182
+ actionShapeIri: string;
183
+ operation: "update" | "addition";
184
+ mapping: Record<string, string>;
185
+ id: string;
186
+ label: string;
187
+ params: {
188
+ id: string;
189
+ type: "string" | "number" | "date" | "iri" | "enum";
190
+ required: boolean;
191
+ classIri?: string | undefined;
192
+ values?: import("zod").JSONType[] | undefined;
193
+ }[];
194
+ confirm: true;
195
+ criteria?: {
196
+ ask: string;
197
+ } | undefined;
198
+ } | {
199
+ id: string;
200
+ ref: {
201
+ item: string;
202
+ revision: string;
203
+ hash: string;
204
+ };
205
+ })[];
206
+ dependencies: {
207
+ kind: "view" | "action" | "query" | "component" | "derivation" | "horn";
208
+ ref: {
209
+ item: string;
210
+ revision: string;
211
+ hash: string;
212
+ };
213
+ }[];
214
+ lock: {
215
+ lockVersion: 1;
216
+ entries: {
217
+ kind: "view" | "action" | "query" | "component" | "derivation" | "horn";
218
+ ref: {
219
+ item: string;
220
+ revision: string;
221
+ hash: string;
222
+ };
223
+ requiredBy: string[];
224
+ }[];
225
+ };
226
+ runtime: {
227
+ kit: string;
228
+ };
229
+ summary?: string | undefined;
230
+ audience?: string | undefined;
231
+ about?: string[] | undefined;
232
+ baseRevision?: number | undefined;
233
+ style?: {
234
+ theme?: "light" | "dark" | undefined;
235
+ accent?: "blue" | "orange" | "green" | "amber" | "pink" | "forest" | "indigo" | "red" | undefined;
236
+ density?: "compact" | "comfortable" | undefined;
237
+ radius?: "sharp" | "soft" | "round" | undefined;
238
+ } | undefined;
239
+ } | undefined;
240
+ pending: boolean;
241
+ };
@@ -0,0 +1,83 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useAgent } from "agents/react";
3
+ import { useAgentChat } from "agents/chat/react";
4
+ import { isPresent } from "@manta-eu/lib/present";
5
+ import { studioReplyOf, studioStepsOf } from "../studio-progress";
6
+ // One array, so a turn with no steps does not re-render the page every tick.
7
+ const emptySteps = [];
8
+ /** The reply and the draft arrive separately; the stream finishes first. */
9
+ const draftGraceMs = 3_000;
10
+ export function useStudioConversation(args) {
11
+ // No leading slash: the socket client builds `${host}/${basePath}` and adds the separator itself.
12
+ const at = `api/d/${encodeURIComponent(args.datastoreId)}/studio/${encodeURIComponent(args.conversation)}`;
13
+ const agent = useAgent({ agent: "studio", name: args.conversation, basePath: at });
14
+ const look = useRef(args.look);
15
+ look.current = args.look;
16
+ const chat = useAgentChat({
17
+ agent,
18
+ credentials: "include",
19
+ getInitialMessages: null,
20
+ resume: true,
21
+ tools: {
22
+ look_at_it: {
23
+ execute: (input) => {
24
+ const why = typeof input === "object" && input !== null && "why" in input ? String(input.why) : "";
25
+ return look.current(why);
26
+ },
27
+ },
28
+ },
29
+ });
30
+ const [tick, setTick] = useState(0);
31
+ const waiting = useRef(undefined);
32
+ const before = useRef(chat.status);
33
+ const drawn = useRef(undefined);
34
+ if (isPresent(agent.state?.draft))
35
+ drawn.current = agent.state.draft;
36
+ useEffect(() => {
37
+ const held = waiting.current;
38
+ if (!isPresent(held)) {
39
+ before.current = chat.status;
40
+ return;
41
+ }
42
+ const settled = chat.status === "ready" || chat.status === "error";
43
+ if (!settled) {
44
+ before.current = chat.status;
45
+ return;
46
+ }
47
+ if (chat.status === "error") {
48
+ waiting.current = undefined;
49
+ held.reject(new Error("The Studio could not answer."));
50
+ return;
51
+ }
52
+ held.settledAt ??= Date.now();
53
+ const changed = drawn.current !== held.before;
54
+ if (!changed && Date.now() - held.settledAt < draftGraceMs) {
55
+ const again = setTimeout(() => setTick((count) => count + 1), 120);
56
+ before.current = chat.status;
57
+ return () => clearTimeout(again);
58
+ }
59
+ waiting.current = undefined;
60
+ const last = [...chat.messages].reverse().find((message) => message.role === "assistant");
61
+ held.resolve({ reply: studioReplyOf(last?.parts ?? []), horn: drawn.current });
62
+ before.current = chat.status;
63
+ return;
64
+ }, [chat.status, chat.messages, tick, agent.state]);
65
+ /** Tells the object which Horn it is about. */
66
+ const open = useCallback(async (horn) => {
67
+ const response = await fetch(`/${at}/open`, {
68
+ method: "POST",
69
+ credentials: "include",
70
+ headers: { "content-type": "application/json" },
71
+ body: JSON.stringify({ horn }),
72
+ });
73
+ if (!response.ok)
74
+ throw new Error("This Horn could not be opened in the Studio.");
75
+ }, [at]);
76
+ const ask = useCallback((said) => new Promise((resolve, reject) => {
77
+ waiting.current = { resolve, reject, before: drawn.current };
78
+ void chat.sendMessage({ text: said });
79
+ }), [chat]);
80
+ const running = chat.status === "ready" ? undefined : [...chat.messages].reverse()[0];
81
+ const steps = running?.role === "assistant" ? studioStepsOf(running.parts ?? []) : emptySteps;
82
+ return { ask, open, steps, draft: agent.state?.draft ?? undefined, pending: chat.status !== "ready" };
83
+ }
@@ -3,5 +3,12 @@ export type StudioStep = {
3
3
  title: string;
4
4
  detail?: string;
5
5
  };
6
- /** One line for one streamed event, or nothing for an event not worth a line of its own. */
7
- export declare function studioProgressLine(line: Record<string, unknown>): StudioStep | undefined;
6
+ export type MessagePart = {
7
+ type: string;
8
+ toolName?: unknown;
9
+ text?: unknown;
10
+ };
11
+ /** What the turn has done so far. The same tool twice running is one line with a count. */
12
+ export declare function studioStepsOf(parts: readonly MessagePart[]): StudioStep[];
13
+ /** The answer: what was said after the last tool call. Anything before it is narration. */
14
+ export declare function studioReplyOf(parts: readonly MessagePart[]): string;
@@ -1,38 +1,63 @@
1
- import { match } from "ts-pattern";
2
- import { isRecord } from "@manta-eu/horns";
3
- // What a Horn turn is doing, in words, from the events the route streams. A tool name is the model's
4
- // vocabulary; the person watching wants to know what is being looked at.
5
1
  const toolLines = {
6
2
  list_classes: "Looking at what this datastore holds",
7
3
  describe_class: "Reading how a kind of thing is described",
8
4
  components_for_class: "Looking for components that already fit",
5
+ find_components: "Looking through the design system",
6
+ read_component_api: "Reading how a component works",
7
+ list_computed: "Looking at what Manta works out",
8
+ list_rules: "Reading the rules",
9
9
  run_sparql: "Querying the datastore",
10
10
  investigate_data: "Investigating the data",
11
+ ask_manta_for_rule: "Asking Manta for a rule",
11
12
  propose_horn_patch: "Changing the Horn",
13
+ propose_plan: "Putting a plan up",
12
14
  propose_library_item: "Offering a part for the library",
13
15
  report_harness_issue: "Noting a problem with the tools",
16
+ look_at_it: "Looking at what it drew",
14
17
  };
15
- // A turn can start a loop of its own. Its steps are the same steps, said by somebody else.
16
- const agentNames = { investigate: "Investigating" };
17
- /** One line for one streamed event, or nothing for an event not worth a line of its own. */
18
- export function studioProgressLine(line) {
19
- const event = line["event"];
20
- if (!isRecord(event))
21
- return undefined;
22
- const name = typeof event["name"] === "string" ? event["name"] : "";
23
- const detail = typeof event["detail"] === "string" ? event["detail"] : undefined;
24
- const agent = typeof event["agent"] === "string" ? (agentNames[event["agent"]] ?? event["agent"]) : undefined;
25
- const step = (title) => title === undefined
26
- ? undefined
27
- : { title: agent === undefined ? title : `${agent} · ${title}`, ...(detail === undefined ? {} : { detail }) };
28
- return match(event["type"])
29
- .with("reading", () => step("Reading the Horn and the data behind it"))
30
- .with("round", () => (event["step"] === 1 && agent === undefined ? step("Thinking") : undefined))
31
- .with("tool-call", () => event["repeated"] === true || name === "propose_horn_patch"
32
- ? undefined
33
- : step(toolLines[name] ?? `Using ${name}`))
34
- .with("proposal-accepted", () => (name === "propose_horn_patch" ? step("Changed the Horn") : undefined))
35
- .with("proposal-rejected", () => step("That change did not hold up against the data, trying again"))
36
- .with("rehydrated", () => step("Starting over from the Horn as it stands"))
37
- .otherwise(() => undefined);
18
+ // A dynamic tool part carries its name separately.
19
+ function toolNameOf(part) {
20
+ if (typeof part.toolName === "string")
21
+ return part.toolName;
22
+ return part.type.startsWith("tool-") ? part.type.slice("tool-".length) : undefined;
23
+ }
24
+ const narrationLimit = 240;
25
+ function wordsOf(part) {
26
+ return typeof part.text === "string" ? part.text.trim() : "";
27
+ }
28
+ /** What the turn has done so far. The same tool twice running is one line with a count. */
29
+ export function studioStepsOf(parts) {
30
+ const steps = [];
31
+ let repeats = 0;
32
+ let narration = "";
33
+ for (const part of parts) {
34
+ const name = toolNameOf(part);
35
+ if (name === undefined) {
36
+ const said = wordsOf(part);
37
+ if (said !== "")
38
+ narration = said;
39
+ continue;
40
+ }
41
+ const title = toolLines[name] ?? `Using ${name}`;
42
+ const detail = narration === "" ? undefined : narration.slice(0, narrationLimit);
43
+ narration = "";
44
+ const last = steps[steps.length - 1];
45
+ if (last?.title === title && detail === undefined) {
46
+ repeats += 1;
47
+ last.detail = `${repeats + 1} times`;
48
+ continue;
49
+ }
50
+ repeats = 0;
51
+ steps.push({ title, ...(detail === undefined ? {} : { detail }) });
52
+ }
53
+ return steps;
54
+ }
55
+ /** The answer: what was said after the last tool call. Anything before it is narration. */
56
+ export function studioReplyOf(parts) {
57
+ const lastTool = parts.map((part) => toolNameOf(part) !== undefined).lastIndexOf(true);
58
+ return parts
59
+ .slice(lastTool + 1)
60
+ .map(wordsOf)
61
+ .filter((said) => said !== "")
62
+ .join("\n\n");
38
63
  }