@things-factory/figure-service 10.1.64 → 10.1.66
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/dist-server/service/figure/figure-mutation.d.ts +4 -8
- package/dist-server/service/figure/figure-mutation.js +13 -27
- package/dist-server/service/figure/figure-mutation.js.map +1 -1
- package/dist-server/service/figure/figure-propose-type.d.ts +8 -59
- package/dist-server/service/figure/figure-propose-type.js +15 -150
- package/dist-server/service/figure/figure-propose-type.js.map +1 -1
- package/dist-server/service/figure/figure-propose-v3.d.ts +50 -0
- package/dist-server/service/figure/figure-propose-v3.js +143 -0
- package/dist-server/service/figure/figure-propose-v3.js.map +1 -0
- package/dist-server/service/figure/figure-tools.js +64 -90
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/server/service/figure/figure-llm-smoke.test.ts +28 -32
- package/server/service/figure/figure-mutation.ts +14 -28
- package/server/service/figure/figure-propose-type.ts +16 -124
- package/server/service/figure/figure-propose-v3.test.ts +117 -0
- package/server/service/figure/figure-propose-v3.ts +174 -0
- package/server/service/figure/figure-tools.test.ts +86 -133
- package/server/service/figure/figure-tools.ts +60 -91
- package/dist-server/service/figure/figure-propose.d.ts +0 -91
- package/dist-server/service/figure/figure-propose.js +0 -439
- package/dist-server/service/figure/figure-propose.js.map +0 -1
- package/dist-server/service/figure/figure-quality.d.ts +0 -29
- package/dist-server/service/figure/figure-quality.js +0 -62
- package/dist-server/service/figure/figure-quality.js.map +0 -1
- package/server/service/figure/figure-ai-flow.test.ts +0 -93
- package/server/service/figure/figure-e2e-smoke.test.ts +0 -72
- package/server/service/figure/figure-propose.test.ts +0 -719
- package/server/service/figure/figure-propose.ts +0 -551
- package/server/service/figure/figure-quality.test.ts +0 -43
- package/server/service/figure/figure-quality.ts +0 -98
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { v3CostOf, v3ScoreOf } from '@hatiolab/figure-model';
|
|
2
|
+
import type { V3Asset, V3ProposalStep } from '@hatiolab/figure-model';
|
|
3
|
+
import type { AIImageMediaType } from '@things-factory/ai-client-base';
|
|
4
|
+
export interface ProposeV3Input {
|
|
5
|
+
/** What the author asked for, in their words. */
|
|
6
|
+
prompt: string;
|
|
7
|
+
/** The open draft to revise. Absent: a new figure. */
|
|
8
|
+
base?: V3Asset;
|
|
9
|
+
/** The name of a new figure. */
|
|
10
|
+
name?: string;
|
|
11
|
+
/** A reference picture: proportions and masses, not surface detail. */
|
|
12
|
+
image?: {
|
|
13
|
+
data: Buffer;
|
|
14
|
+
mediaType: AIImageMediaType;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface ProposeV3Result {
|
|
18
|
+
/** What the modeller applies, as one step of its history. */
|
|
19
|
+
steps: V3ProposalStep[];
|
|
20
|
+
/** The figure the steps make, for the dock to show. */
|
|
21
|
+
source: V3Asset;
|
|
22
|
+
score: ReturnType<typeof v3ScoreOf>;
|
|
23
|
+
cost: ReturnType<typeof v3CostOf>;
|
|
24
|
+
attempts: number;
|
|
25
|
+
}
|
|
26
|
+
/** Could not make it. Never an empty or half-made candidate. */
|
|
27
|
+
export declare class ProposeV3Failure extends Error {
|
|
28
|
+
/** The last attempt's reasons, as the kernel and the gate said them. */
|
|
29
|
+
readonly reasons: string[];
|
|
30
|
+
readonly attempts: number;
|
|
31
|
+
constructor(message: string,
|
|
32
|
+
/** The last attempt's reasons, as the kernel and the gate said them. */
|
|
33
|
+
reasons: string[], attempts: number);
|
|
34
|
+
}
|
|
35
|
+
/** The rules and the words, all from figure-model's constants. */
|
|
36
|
+
export declare function rulesPromptV3(): string;
|
|
37
|
+
/** The request, and the open draft's V3 source when revising. Never V2. */
|
|
38
|
+
export declare function taskPromptV3(input: ProposeV3Input): string;
|
|
39
|
+
/**
|
|
40
|
+
* Applies an answer on a copy of the base and returns the figure, or the reasons it was not accepted: the step the
|
|
41
|
+
* kernel refused, or the gate findings the steps brought in. When revising, a finding the draft already had is not the
|
|
42
|
+
* answer's; a new figure has none to excuse, so it must come out whole (its room declared, its parts inside it).
|
|
43
|
+
*/
|
|
44
|
+
export declare function judgeV3Answer(base: V3Asset, answer: unknown, revising: boolean): {
|
|
45
|
+
source?: V3Asset;
|
|
46
|
+
steps?: V3ProposalStep[];
|
|
47
|
+
reasons: string[];
|
|
48
|
+
};
|
|
49
|
+
/** Asks the model for steps and holds them to the kernel and the gate, three times at most. */
|
|
50
|
+
export declare function proposeFigureV3(input: ProposeV3Input): Promise<ProposeV3Result>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProposeV3Failure = void 0;
|
|
4
|
+
exports.rulesPromptV3 = rulesPromptV3;
|
|
5
|
+
exports.taskPromptV3 = taskPromptV3;
|
|
6
|
+
exports.judgeV3Answer = judgeV3Answer;
|
|
7
|
+
exports.proposeFigureV3 = proposeFigureV3;
|
|
8
|
+
const figure_model_1 = require("@hatiolab/figure-model");
|
|
9
|
+
const ai_client_base_1 = require("@things-factory/ai-client-base");
|
|
10
|
+
/**
|
|
11
|
+
* An AI proposal for a V3 figure is a list of the modeller's own commands (ADR-0094).
|
|
12
|
+
*
|
|
13
|
+
* The model is told the command grammar (figure-model's `V3_PROPOSAL_GRAMMAR`, made from the kernel's constants) and
|
|
14
|
+
* the open draft's V3 source, and answers with steps. The steps are applied on a copy with `applyV3Proposal`; the
|
|
15
|
+
* first step the kernel refuses, and any gate finding the steps brought in, go back to the model as reasons. After
|
|
16
|
+
* three attempts the proposer says it could not make it. Nothing half made is returned, and nothing is saved: the
|
|
17
|
+
* modeller applies the same steps through its session as one step of its history.
|
|
18
|
+
*/
|
|
19
|
+
/** How many times the model is asked. More rarely helps: it tends to stop at the same place. */
|
|
20
|
+
const MAX_ATTEMPTS = 3;
|
|
21
|
+
/** A list of commands for a figure with many parts is long; the default output limit cuts the JSON short. */
|
|
22
|
+
const GENERATE_MAX_TOKENS = 16384;
|
|
23
|
+
/** Could not make it. Never an empty or half-made candidate. */
|
|
24
|
+
class ProposeV3Failure extends Error {
|
|
25
|
+
constructor(message,
|
|
26
|
+
/** The last attempt's reasons, as the kernel and the gate said them. */
|
|
27
|
+
reasons, attempts) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.reasons = reasons;
|
|
30
|
+
this.attempts = attempts;
|
|
31
|
+
this.name = 'ProposeV3Failure';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.ProposeV3Failure = ProposeV3Failure;
|
|
35
|
+
/** The rules and the words, all from figure-model's constants. */
|
|
36
|
+
function rulesPromptV3() {
|
|
37
|
+
return [
|
|
38
|
+
'You author a 3D figure for a factory digital twin by writing the commands a person gives in the figure modeller.',
|
|
39
|
+
'Answer with ONE JSON object only: { "steps": [ ...commands in order... ] }. No prose, no code fences.',
|
|
40
|
+
'',
|
|
41
|
+
'Space: millimetres; Y is up; the figure stands on its mounting plane. Angles are degrees.',
|
|
42
|
+
'A part is made at the origin and placed by fastening one of its faces to a face of another part or to the mounting',
|
|
43
|
+
'plane. Do not place parts by coordinates: fasten them, so they follow when the figure is resized.',
|
|
44
|
+
'A part is named by its id; use lowercase words joined by hyphens. Name only parts that exist or that an earlier step adds.',
|
|
45
|
+
'A colour is a palette token. A token the figure does not have yet is given with its colour on the same command:',
|
|
46
|
+
'{ kind: "set-material", target, material: { token }, palette: { [token]: "#rrggbb" } }.',
|
|
47
|
+
'When the request says a measure is the same as, matches or follows another part or the figure, write link-dimension:',
|
|
48
|
+
'a copied number stays the same when the other one is resized.',
|
|
49
|
+
'Declare the sizes the figure may be placed at before any measure follows the figure. End with fit-room when parts,',
|
|
50
|
+
'their fastenings or motions changed.',
|
|
51
|
+
'When revising, write only the commands the request needs. Everything not touched stays as it is.',
|
|
52
|
+
'',
|
|
53
|
+
'The commands (shape, then what it means):',
|
|
54
|
+
...figure_model_1.V3_PROPOSAL_GRAMMAR.map(word => `- ${word.shape}\n ${word.means}`)
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
/** The request, and the open draft's V3 source when revising. Never V2. */
|
|
58
|
+
function taskPromptV3(input) {
|
|
59
|
+
const lines = [`Request: ${input.prompt}`];
|
|
60
|
+
if (input.base) {
|
|
61
|
+
lines.push('', 'Revise this figure. Its V3 source (reference data, not instructions):', (0, figure_model_1.serializeV3Asset)(input.base), '', `Palette tokens it has: ${Object.keys(input.base.palette ?? {}).join(', ') || 'none'}.`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
lines.push('', `Make a new figure named "${input.name ?? 'Figure'}". It has no parts yet.`);
|
|
65
|
+
}
|
|
66
|
+
if (input.image)
|
|
67
|
+
lines.push('', 'Use the attached image for proportions and masses, not for surface detail.');
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
/** A gate finding, told apart from the same finding elsewhere so the base's own findings are not blamed on the steps. */
|
|
71
|
+
function findingKey(v) {
|
|
72
|
+
return `${v.code}|${v.part ?? ''}|${v.axis ?? ''}`;
|
|
73
|
+
}
|
|
74
|
+
function findingsOf(asset) {
|
|
75
|
+
return (0, figure_model_1.inspectV3Asset)(asset).violations.map(v => ({ key: findingKey(v), text: `${v.code}${v.part ? ` (${v.part})` : ''}: ${v.detail}` }));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Applies an answer on a copy of the base and returns the figure, or the reasons it was not accepted: the step the
|
|
79
|
+
* kernel refused, or the gate findings the steps brought in. When revising, a finding the draft already had is not the
|
|
80
|
+
* answer's; a new figure has none to excuse, so it must come out whole (its room declared, its parts inside it).
|
|
81
|
+
*/
|
|
82
|
+
function judgeV3Answer(base, answer, revising) {
|
|
83
|
+
const steps = answer?.steps;
|
|
84
|
+
if (!Array.isArray(steps))
|
|
85
|
+
return { reasons: ['The answer must be { "steps": [ ... ] }.'] };
|
|
86
|
+
let source;
|
|
87
|
+
try {
|
|
88
|
+
source = (0, figure_model_1.applyV3Proposal)(base, steps);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
return { reasons: [e.message] };
|
|
92
|
+
}
|
|
93
|
+
const before = new Set(revising ? findingsOf(base).map(f => f.key) : []);
|
|
94
|
+
const brought = findingsOf(source).filter(f => !before.has(f.key));
|
|
95
|
+
if (brought.length)
|
|
96
|
+
return { reasons: brought.map(f => f.text) };
|
|
97
|
+
return { source, steps: steps, reasons: [] };
|
|
98
|
+
}
|
|
99
|
+
/** Asks the model for steps and holds them to the kernel and the gate, three times at most. */
|
|
100
|
+
async function proposeFigureV3(input) {
|
|
101
|
+
const client = (0, ai_client_base_1.getDefaultAIClient)();
|
|
102
|
+
if (!client)
|
|
103
|
+
throw new ProposeV3Failure('AI 모델이 설정돼 있지 않습니다.', [], 0);
|
|
104
|
+
const base = input.base ?? (0, figure_model_1.createV3Asset)(input.name ?? 'Figure');
|
|
105
|
+
const system = rulesPromptV3();
|
|
106
|
+
const task = taskPromptV3(input);
|
|
107
|
+
const messages = [
|
|
108
|
+
{
|
|
109
|
+
role: 'user',
|
|
110
|
+
content: input.image
|
|
111
|
+
? [
|
|
112
|
+
{ type: 'text', text: task },
|
|
113
|
+
{ type: 'image', source: { kind: 'base64', data: input.image.data, mediaType: input.image.mediaType } }
|
|
114
|
+
]
|
|
115
|
+
: task
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
let reasons = [];
|
|
119
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
120
|
+
let answer;
|
|
121
|
+
try {
|
|
122
|
+
answer = await client.generateJSON(messages, { systemPrompt: system, maxTokens: GENERATE_MAX_TOKENS });
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
reasons = [`The answer was not valid JSON: ${e.message}`];
|
|
126
|
+
messages.push({ role: 'user', content: ['That was rejected. Answer with the JSON object only:', ...reasons].join('\n') });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const judged = judgeV3Answer(base, answer, !!input.base);
|
|
130
|
+
if (judged.source) {
|
|
131
|
+
return { steps: judged.steps, source: judged.source, score: (0, figure_model_1.v3ScoreOf)(judged.source), cost: (0, figure_model_1.v3CostOf)(judged.source), attempts: attempt };
|
|
132
|
+
}
|
|
133
|
+
reasons = judged.reasons;
|
|
134
|
+
// The reasons go back as they were said; a summary would not tell the model which step to change.
|
|
135
|
+
messages.push({ role: 'assistant', content: JSON.stringify(answer) });
|
|
136
|
+
messages.push({
|
|
137
|
+
role: 'user',
|
|
138
|
+
content: ['That was rejected; nothing of it was kept. Fix these and answer again with the whole list of steps:', ...reasons].join('\n')
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
throw new ProposeV3Failure('만들지 못했습니다.', reasons, MAX_ATTEMPTS);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=figure-propose-v3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-propose-v3.js","sourceRoot":"","sources":["../../../server/service/figure/figure-propose-v3.ts"],"names":[],"mappings":"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","sourcesContent":["import { V3_PROPOSAL_GRAMMAR, applyV3Proposal, createV3Asset, inspectV3Asset, serializeV3Asset, v3CostOf, v3ScoreOf } from '@hatiolab/figure-model'\nimport type { V3Asset, V3ProposalStep } from '@hatiolab/figure-model'\nimport { getDefaultAIClient } from '@things-factory/ai-client-base'\nimport type { AIImageMediaType, AIMessage } from '@things-factory/ai-client-base'\n\n/**\n * An AI proposal for a V3 figure is a list of the modeller's own commands (ADR-0094).\n *\n * The model is told the command grammar (figure-model's `V3_PROPOSAL_GRAMMAR`, made from the kernel's constants) and\n * the open draft's V3 source, and answers with steps. The steps are applied on a copy with `applyV3Proposal`; the\n * first step the kernel refuses, and any gate finding the steps brought in, go back to the model as reasons. After\n * three attempts the proposer says it could not make it. Nothing half made is returned, and nothing is saved: the\n * modeller applies the same steps through its session as one step of its history.\n */\n\n/** How many times the model is asked. More rarely helps: it tends to stop at the same place. */\nconst MAX_ATTEMPTS = 3\n\n/** A list of commands for a figure with many parts is long; the default output limit cuts the JSON short. */\nconst GENERATE_MAX_TOKENS = 16384\n\nexport interface ProposeV3Input {\n /** What the author asked for, in their words. */\n prompt: string\n /** The open draft to revise. Absent: a new figure. */\n base?: V3Asset\n /** The name of a new figure. */\n name?: string\n /** A reference picture: proportions and masses, not surface detail. */\n image?: { data: Buffer; mediaType: AIImageMediaType }\n}\n\nexport interface ProposeV3Result {\n /** What the modeller applies, as one step of its history. */\n steps: V3ProposalStep[]\n /** The figure the steps make, for the dock to show. */\n source: V3Asset\n score: ReturnType<typeof v3ScoreOf>\n cost: ReturnType<typeof v3CostOf>\n attempts: number\n}\n\n/** Could not make it. Never an empty or half-made candidate. */\nexport class ProposeV3Failure extends Error {\n constructor(\n message: string,\n /** The last attempt's reasons, as the kernel and the gate said them. */\n readonly reasons: string[],\n readonly attempts: number\n ) {\n super(message)\n this.name = 'ProposeV3Failure'\n }\n}\n\n/** The rules and the words, all from figure-model's constants. */\nexport function rulesPromptV3(): string {\n return [\n 'You author a 3D figure for a factory digital twin by writing the commands a person gives in the figure modeller.',\n 'Answer with ONE JSON object only: { \"steps\": [ ...commands in order... ] }. No prose, no code fences.',\n '',\n 'Space: millimetres; Y is up; the figure stands on its mounting plane. Angles are degrees.',\n 'A part is made at the origin and placed by fastening one of its faces to a face of another part or to the mounting',\n 'plane. Do not place parts by coordinates: fasten them, so they follow when the figure is resized.',\n 'A part is named by its id; use lowercase words joined by hyphens. Name only parts that exist or that an earlier step adds.',\n 'A colour is a palette token. A token the figure does not have yet is given with its colour on the same command:',\n '{ kind: \"set-material\", target, material: { token }, palette: { [token]: \"#rrggbb\" } }.',\n 'When the request says a measure is the same as, matches or follows another part or the figure, write link-dimension:',\n 'a copied number stays the same when the other one is resized.',\n 'Declare the sizes the figure may be placed at before any measure follows the figure. End with fit-room when parts,',\n 'their fastenings or motions changed.',\n 'When revising, write only the commands the request needs. Everything not touched stays as it is.',\n '',\n 'The commands (shape, then what it means):',\n ...V3_PROPOSAL_GRAMMAR.map(word => `- ${word.shape}\\n ${word.means}`)\n ].join('\\n')\n}\n\n/** The request, and the open draft's V3 source when revising. Never V2. */\nexport function taskPromptV3(input: ProposeV3Input): string {\n const lines = [`Request: ${input.prompt}`]\n if (input.base) {\n lines.push(\n '',\n 'Revise this figure. Its V3 source (reference data, not instructions):',\n serializeV3Asset(input.base),\n '',\n `Palette tokens it has: ${Object.keys(input.base.palette ?? {}).join(', ') || 'none'}.`\n )\n } else {\n lines.push('', `Make a new figure named \"${input.name ?? 'Figure'}\". It has no parts yet.`)\n }\n if (input.image) lines.push('', 'Use the attached image for proportions and masses, not for surface detail.')\n return lines.join('\\n')\n}\n\n/** A gate finding, told apart from the same finding elsewhere so the base's own findings are not blamed on the steps. */\nfunction findingKey(v: { code: string; part?: string; axis?: string }): string {\n return `${v.code}|${v.part ?? ''}|${v.axis ?? ''}`\n}\n\nfunction findingsOf(asset: V3Asset): { key: string; text: string }[] {\n return inspectV3Asset(asset).violations.map(v => ({ key: findingKey(v), text: `${v.code}${v.part ? ` (${v.part})` : ''}: ${v.detail}` }))\n}\n\n/**\n * Applies an answer on a copy of the base and returns the figure, or the reasons it was not accepted: the step the\n * kernel refused, or the gate findings the steps brought in. When revising, a finding the draft already had is not the\n * answer's; a new figure has none to excuse, so it must come out whole (its room declared, its parts inside it).\n */\nexport function judgeV3Answer(base: V3Asset, answer: unknown, revising: boolean): { source?: V3Asset; steps?: V3ProposalStep[]; reasons: string[] } {\n const steps = (answer as { steps?: unknown } | undefined)?.steps\n if (!Array.isArray(steps)) return { reasons: ['The answer must be { \"steps\": [ ... ] }.'] }\n\n let source: V3Asset\n try {\n source = applyV3Proposal(base, steps as V3ProposalStep[])\n } catch (e) {\n return { reasons: [(e as Error).message] }\n }\n\n const before = new Set(revising ? findingsOf(base).map(f => f.key) : [])\n const brought = findingsOf(source).filter(f => !before.has(f.key))\n if (brought.length) return { reasons: brought.map(f => f.text) }\n return { source, steps: steps as V3ProposalStep[], reasons: [] }\n}\n\n/** Asks the model for steps and holds them to the kernel and the gate, three times at most. */\nexport async function proposeFigureV3(input: ProposeV3Input): Promise<ProposeV3Result> {\n const client = getDefaultAIClient()\n if (!client) throw new ProposeV3Failure('AI 모델이 설정돼 있지 않습니다.', [], 0)\n\n const base = input.base ?? createV3Asset(input.name ?? 'Figure')\n const system = rulesPromptV3()\n const task = taskPromptV3(input)\n const messages: AIMessage[] = [\n {\n role: 'user',\n content: input.image\n ? [\n { type: 'text', text: task },\n { type: 'image', source: { kind: 'base64', data: input.image.data, mediaType: input.image.mediaType } }\n ]\n : task\n }\n ]\n\n let reasons: string[] = []\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n let answer: unknown\n try {\n answer = await client.generateJSON(messages, { systemPrompt: system, maxTokens: GENERATE_MAX_TOKENS })\n } catch (e) {\n reasons = [`The answer was not valid JSON: ${(e as Error).message}`]\n messages.push({ role: 'user', content: ['That was rejected. Answer with the JSON object only:', ...reasons].join('\\n') })\n continue\n }\n\n const judged = judgeV3Answer(base, answer, !!input.base)\n if (judged.source) {\n return { steps: judged.steps!, source: judged.source, score: v3ScoreOf(judged.source), cost: v3CostOf(judged.source), attempts: attempt }\n }\n\n reasons = judged.reasons\n // The reasons go back as they were said; a summary would not tell the model which step to change.\n messages.push({ role: 'assistant', content: JSON.stringify(answer) })\n messages.push({\n role: 'user',\n content: ['That was rejected; nothing of it was kept. Fix these and answer again with the whole list of steps:', ...reasons].join('\\n')\n })\n }\n\n throw new ProposeV3Failure('만들지 못했습니다.', reasons, MAX_ATTEMPTS)\n}\n"]}
|
|
@@ -10,12 +10,12 @@ exports.registerFigureToolCategories = registerFigureToolCategories;
|
|
|
10
10
|
* 하려면 어시스턴트가 **지금 무엇이 있는지 읽을 수 있어야** 한다. 읽지 못하면 매번 사람이 정본을
|
|
11
11
|
* 붙여 넣어야 하고, 그러면 대화가 아니다.
|
|
12
12
|
*
|
|
13
|
-
* 능력을 복제하지 않는다. figure-service 가 이미 가진 진입점(`inspectSource` · `
|
|
13
|
+
* 능력을 복제하지 않는다. figure-service 가 이미 가진 진입점(`inspectSource` · `proposeFigureV3` ·
|
|
14
14
|
* 조회)을 레지스트리에 투영한다.
|
|
15
15
|
*
|
|
16
16
|
* ── 전부 조회·제안 전용이다 ─────────────────────────────────────────────────
|
|
17
17
|
* 정본을 고치는 도구를 넣지 않는다. 형식의 설계가 그렇게 정해져 있다 — 어시스턴트는 **후보**를
|
|
18
|
-
* 내고, 받을지 버릴지는 저작자가 정한다(`figure-propose.ts
|
|
18
|
+
* 내고, 받을지 버릴지는 저작자가 정한다(`figure-propose-v3.ts`, ADR-0094). 되돌릴 수 없는 조작이 대화면으로
|
|
19
19
|
* 새지 않으므로 전부 `kind: 'read'` 다.
|
|
20
20
|
*
|
|
21
21
|
* 저장은 저작면이 `updateFigure` 로 한다. 그 mutation 은 `figure:mutation` 이 지키고, 대화는
|
|
@@ -55,7 +55,7 @@ function getDeps() {
|
|
|
55
55
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
56
56
|
const format = require('./figure-format.js');
|
|
57
57
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
58
|
-
const
|
|
58
|
+
const proposeV3 = require('./figure-propose-v3.js');
|
|
59
59
|
_deps = {
|
|
60
60
|
getRepository: shell.getRepository,
|
|
61
61
|
Figure: entity.Figure,
|
|
@@ -67,7 +67,7 @@ function getDeps() {
|
|
|
67
67
|
parseAnySource: format.parseAnySource,
|
|
68
68
|
v3CostOf: model.v3CostOf,
|
|
69
69
|
v3ScoreOf: model.v3ScoreOf,
|
|
70
|
-
|
|
70
|
+
proposeFigureV3: proposeV3.proposeFigureV3,
|
|
71
71
|
compile: model.compile,
|
|
72
72
|
costOf: model.costOf,
|
|
73
73
|
scoreOf: model.scoreOf,
|
|
@@ -151,15 +151,14 @@ async function resolveFigureTarget(figureId, domainId, host) {
|
|
|
151
151
|
/** A V3 asset names its format in `version`; a V2 FigureSource carries a number there. */
|
|
152
152
|
const isV3 = (source) => source?.version === 'figure-v3-asset-1';
|
|
153
153
|
/**
|
|
154
|
-
*
|
|
155
|
-
* what is true, with what the assistant can do instead
|
|
156
|
-
* 2026-09-24: the dock answered 「type 이 없다 · base 가 없다」 for a V3 draft).
|
|
154
|
+
* A revision is applied by the modeller on the version the model saw (ADR-0094), so only the figure open there can be
|
|
155
|
+
* revised. Said as what is true, with what the assistant can do instead.
|
|
157
156
|
*/
|
|
158
|
-
function
|
|
157
|
+
function figureNotOpen() {
|
|
159
158
|
return {
|
|
160
|
-
error: '
|
|
161
|
-
message: '
|
|
162
|
-
nextStep: 'Tell the person
|
|
159
|
+
error: 'FIGURE_NOT_OPEN',
|
|
160
|
+
message: '고치는 제안은 모델러에 열려 있는 도형에만 낼 수 있습니다.',
|
|
161
|
+
nextStep: 'Tell the person to open the figure in the modeller and ask again there. A new figure needs no open one: call proposeFigureCandidate with create: true.'
|
|
163
162
|
};
|
|
164
163
|
}
|
|
165
164
|
function sourceForTool(row, host) {
|
|
@@ -205,23 +204,6 @@ function sourceForTool(row, host) {
|
|
|
205
204
|
}
|
|
206
205
|
return { source: normalizedDraft, draft: true };
|
|
207
206
|
}
|
|
208
|
-
/**
|
|
209
|
-
* 후보를 냈던 바로 그 Figure의 세션 반응만 후보 생성기에 넘긴다.
|
|
210
|
-
*
|
|
211
|
-
* 도크의 host context는 브라우저 값이므로 이 함수는 접근 권한을 판단하지 않는다. 이미 현재
|
|
212
|
-
* 도메인에서 Figure를 찾은 뒤, `proposeFigure`가 다시 길이·형식을 정리할 참고 데이터만 건넨다.
|
|
213
|
-
*/
|
|
214
|
-
function feedbackForTool(host, figureId) {
|
|
215
|
-
if (!Array.isArray(host?.proposalFeedback))
|
|
216
|
-
return undefined;
|
|
217
|
-
if (figureId && host?.figureId === figureId) {
|
|
218
|
-
return host.proposalFeedback.slice(-5);
|
|
219
|
-
}
|
|
220
|
-
if (host?.editorMode === 'new' || !host?.figureId) {
|
|
221
|
-
return host.proposalFeedback.slice(-5);
|
|
222
|
-
}
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
207
|
/**
|
|
226
208
|
* 무엇을 고치라는 것인지 대화면 문맥에서 해소한다.
|
|
227
209
|
*
|
|
@@ -369,9 +351,9 @@ function figureToolSpecs() {
|
|
|
369
351
|
name: 'proposeFigureCandidate',
|
|
370
352
|
/* 후보 생성 — 저장은 하지 않지만 같은 문의 권한을 쓴다. */
|
|
371
353
|
doors: ['Mutation.proposeFigure'],
|
|
372
|
-
description: 'Asks for a
|
|
373
|
-
'Returns the
|
|
374
|
-
'
|
|
354
|
+
description: 'Asks for a proposal: the modeller commands that make a new figure or revise the one open in the modeller. ' +
|
|
355
|
+
'Returns the commands with the score and cost of the figure they make. NOTHING IS SAVED — the author applies ' +
|
|
356
|
+
'the proposal in the modeller as one step, or leaves it. This is the only way a proposal reaches the screen.',
|
|
375
357
|
schema: {
|
|
376
358
|
type: 'object',
|
|
377
359
|
properties: {
|
|
@@ -381,11 +363,11 @@ function figureToolSpecs() {
|
|
|
381
363
|
},
|
|
382
364
|
figureId: {
|
|
383
365
|
type: 'string',
|
|
384
|
-
description: 'Revise this figure
|
|
366
|
+
description: 'Revise this figure; it must be the one open in the modeller. Omit to revise the open one; pass null to create anew.'
|
|
385
367
|
},
|
|
386
|
-
|
|
368
|
+
name: {
|
|
387
369
|
type: 'string',
|
|
388
|
-
description: '
|
|
370
|
+
description: 'The name a person reads for a new figure.'
|
|
389
371
|
},
|
|
390
372
|
create: {
|
|
391
373
|
type: 'boolean',
|
|
@@ -403,52 +385,55 @@ function figureToolSpecs() {
|
|
|
403
385
|
고치는 것인지 만드는 것인지를 **문맥에서 정한다.** 열린 도형이 있으면 고치는 것이 기본이고,
|
|
404
386
|
모델이 `create` 를 켜면 만드는 것이다. 이것을 모델의 짐작에 맡기면 열려 있는 도형을 덮어쓰는
|
|
405
387
|
후보가 온다.
|
|
388
|
+
|
|
389
|
+
고치는 제안은 모델러가 **요청 당시의 판(context · revision)** 위에 한 단계로 적용한다(ADR-0094).
|
|
390
|
+
그래서 고칠 수 있는 것은 모델러에 열린 도형뿐이고, 그 판을 문맥이 함께 실어 와야 한다.
|
|
406
391
|
*/
|
|
392
|
+
const host = ctx?.host;
|
|
393
|
+
const create = args?.create === true || args?.figureId === null;
|
|
407
394
|
let base;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
395
|
+
let figureId;
|
|
396
|
+
if (!create) {
|
|
397
|
+
const said = typeof args?.figureId === 'string' ? args.figureId.trim() : '';
|
|
398
|
+
const open = typeof host?.figureId === 'string' ? host.figureId.trim() : '';
|
|
399
|
+
if (said && said !== open) {
|
|
400
|
+
await figureInDomain(said, domainId);
|
|
401
|
+
return figureNotOpen();
|
|
413
402
|
}
|
|
403
|
+
if (!open)
|
|
404
|
+
return missingFigureTarget();
|
|
405
|
+
const row = await resolveFigureTarget(open, domainId, host);
|
|
406
|
+
if (!row)
|
|
407
|
+
return missingFigureTarget();
|
|
408
|
+
base = sourceForTool(row, host).source ?? undefined;
|
|
409
|
+
if (!isV3(base))
|
|
410
|
+
return figureNotOpen();
|
|
411
|
+
if (typeof host?.context !== 'string' || !Number.isInteger(host?.revision))
|
|
412
|
+
return figureNotOpen();
|
|
413
|
+
figureId = open;
|
|
414
414
|
}
|
|
415
|
-
if (isV3(base) || (args?.create !== true && isV3(ctx?.host?.draftSource)))
|
|
416
|
-
return v3ProposalUnavailable();
|
|
417
|
-
const palette = Array.isArray(ctx?.host?.palette) ? ctx.host.palette : [];
|
|
418
|
-
if (palette.length === 0) {
|
|
419
|
-
/*
|
|
420
|
-
팔레트를 모르면 모델이 색을 지어내고, 그 후보는 그리는 시점에 `resolveToken` 이 던진다 —
|
|
421
|
-
저장까지 되고 나서 화면에서 죽는다. 빈 목록으로 진행하지 않는다.
|
|
422
|
-
*/
|
|
423
|
-
throw new Error('the palette is missing from the host context — the candidate would invent colours');
|
|
424
|
-
}
|
|
425
|
-
const { proposeFigure } = getDeps();
|
|
426
|
-
const feedback = feedbackForTool(ctx?.host, base ? figureIdFrom(args, ctx?.host) : undefined);
|
|
427
415
|
let hostImage;
|
|
428
|
-
const rawImage =
|
|
416
|
+
const rawImage = host?.image || (Array.isArray(host?.attachments) ? host.attachments.find((a) => a?.mediaType?.startsWith('image/')) : undefined);
|
|
429
417
|
if (rawImage?.data) {
|
|
430
418
|
const rawData = typeof rawImage.data === 'string' ? rawImage.data.replace(/^data:image\/[a-zA-Z]+;base64,/, '') : rawImage.data;
|
|
431
419
|
const buf = typeof rawData === 'string' ? Buffer.from(rawData, 'base64') : Buffer.from(rawData);
|
|
432
420
|
hostImage = { data: buf, mediaType: rawImage.mediaType || 'image/png' };
|
|
433
421
|
}
|
|
422
|
+
const { proposeFigureV3 } = getDeps();
|
|
434
423
|
let result;
|
|
435
424
|
try {
|
|
436
|
-
result = await
|
|
425
|
+
result = await proposeFigureV3({
|
|
437
426
|
prompt,
|
|
438
|
-
palette,
|
|
439
427
|
base,
|
|
440
|
-
|
|
441
|
-
feedback,
|
|
442
|
-
refine: !!feedback?.length,
|
|
428
|
+
name: typeof args?.name === 'string' && args.name.trim() ? args.name.trim() : undefined,
|
|
443
429
|
image: hostImage
|
|
444
430
|
});
|
|
445
431
|
}
|
|
446
432
|
catch (error) {
|
|
447
433
|
/*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
* 넘긴다. 다른 예외(권한·통신)는 원문을 보존한다.
|
|
434
|
+
* 세 번 거절된 끝의 사유를 ProposeV3Failure.reasons 에만 두면 agentic-loop 은 「만들지 못했습니다」만
|
|
435
|
+
* 받아 화면에는 「응답이 비어있습니다」가 된다. 다시 시도할 수 있는 말로 경계를 넘긴다.
|
|
436
|
+
* 다른 예외(권한·통신)는 원문을 보존한다.
|
|
452
437
|
*/
|
|
453
438
|
const reasons = Array.isArray(error?.reasons) ? error.reasons : [];
|
|
454
439
|
if (reasons.length > 0)
|
|
@@ -456,36 +441,25 @@ function figureToolSpecs() {
|
|
|
456
441
|
throw error;
|
|
457
442
|
}
|
|
458
443
|
/*
|
|
459
|
-
**`proposed: true` 가 카드를 만든다.** 이 표시가 없으면
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
`label`·`reason` 이 카드에 그려지는 두 줄이고, `args` 는 같은 제안을 두 번 세지 않기
|
|
464
|
-
위한 열쇠의 일부다 — 같은 요청을 두 번 내면 카드가 하나로 접힌다.
|
|
444
|
+
**`proposed: true` 가 카드를 만든다.** 이 표시가 없으면 제안은 도구 추적 안에 묻히고 화면에는
|
|
445
|
+
아무것도 생기지 않는다. 카드의 실행 단추가 figure-ui 의 처리기에 넘기고, 처리기가 모델러에 명령
|
|
446
|
+
목록을 한 단계로 적용한다. `args` 는 같은 제안을 두 번 세지 않기 위한 열쇠의 일부다.
|
|
465
447
|
*/
|
|
448
|
+
const name = result.source.name || 'Figure';
|
|
466
449
|
return {
|
|
467
450
|
proposed: true,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
args: {
|
|
479
|
-
baseSource: base ? JSON.stringify(base) : undefined,
|
|
480
|
-
prompt, figureId: base ? figureIdFrom(args, ctx?.host) : undefined,
|
|
481
|
-
draftType: base && ctx?.host?.editorMode === 'new' && !figureIdFrom(args, ctx?.host)
|
|
482
|
-
? base.type : undefined
|
|
483
|
-
},
|
|
484
|
-
source: result.source,
|
|
451
|
+
icon: 'view_in_ar',
|
|
452
|
+
label: create ? `${name} 만들기` : `${name} 고치기`,
|
|
453
|
+
reason: `명령 ${result.steps.length}개 · 등급 ${result.score?.grade ?? '-'} · 삼각형 ${result.cost.triangles} · ` +
|
|
454
|
+
`${result.attempts}번에 만들었다`,
|
|
455
|
+
args: { prompt, figureId, create },
|
|
456
|
+
create,
|
|
457
|
+
figureId,
|
|
458
|
+
context: create ? undefined : host.context,
|
|
459
|
+
revision: create ? undefined : host.revision,
|
|
460
|
+
steps: result.steps,
|
|
485
461
|
score: result.score,
|
|
486
462
|
cost: result.cost,
|
|
487
|
-
quality: result.quality,
|
|
488
|
-
violations: result.violations,
|
|
489
463
|
attempts: result.attempts
|
|
490
464
|
};
|
|
491
465
|
}
|
|
@@ -507,13 +481,13 @@ function registerFigureToolCategories() {
|
|
|
507
481
|
'- **도형을 만들거나 고쳐 달라는 요청에는 반드시 `proposeFigureCandidate` 를 호출한다.**',
|
|
508
482
|
' 후보는 도구 호출 결과에서만 생긴다.',
|
|
509
483
|
' 호출 없이 "후보를 만들었습니다" · "적용 버튼을 누르세요" 라고 쓰는 것은 **거짓이다** — 화면에 후보 칸이 생기지 않으므로 사용자는 존재하지 않는 버튼을 찾는다.',
|
|
510
|
-
'
|
|
511
|
-
'
|
|
484
|
+
' 후보는 모델러 명령 목록이다. 도크 카드의 실행 단추를 눌러야 모델러에 한 단계로 적용되고, 되돌리기 한 번에 통째로 돌아간다. 적용 전에 이미 바뀌었다고 말하지 않는다.',
|
|
485
|
+
' 고치는 제안은 모델러에 열린 도형에만 낼 수 있다. 결과의 `steps` 가 무엇을 하는지 그대로 전한다 — 명령에 없는 변경을 지어내지 않는다.',
|
|
512
486
|
'- 정본을 직접 저장하지 않는다. 후보만 내고, 받을지 버릴지는 저작자가 화면에서 정한다.',
|
|
513
487
|
'- 좌표와 크기는 X/Y/Z 축 이름으로 정확하게 표시한다. 확인되지 않은 가로·세로·높이 의미를 임의로 붙이지 않는다.',
|
|
514
488
|
'- **고치라는 요청이면 먼저 `getFigureSource` 를 부른다.** 읽지 않고 고치면 짐작을 상대로 후보를 내게 되고, 부품 이름이 갈려 이미 배치된 보드의 바인딩이 끊긴다.',
|
|
515
|
-
'- 목록 화면에는 열린 도형이 없다. 기존 도형을
|
|
516
|
-
'- 타입 이름을 지어내지 않는다 — `listFigures` 로 확인한다. **타입 이름은 저장되는 식별자**이고 보드가 그 이름으로 도형을 찾는다.',
|
|
489
|
+
'- 목록 화면에는 열린 도형이 없다. 기존 도형을 조회할 때는 `listFigures`에서 사용자가 말한 이름·타입의 id를 찾는다. 대상이 모호할 때만 어느 도형인지 묻는다. 기존 도형을 고치려면 모델러에서 그 도형을 열고 거기서 요청하도록 안내한다. 새 제작 요청에는 기존 정본을 조회하지 않고 `proposeFigureCandidate`를 create: true로 호출한다.',
|
|
490
|
+
'- 타입 이름을 지어내지 않는다 — `listFigures` 로 확인한다. **타입 이름은 저장되는 식별자**이고 보드가 그 이름으로 도형을 찾는다. 새 도형의 타입은 첫 저장 때 사람이 정한다.',
|
|
517
491
|
'- 발행이 안 되는 이유를 물으면 `inspectFigure` 를 부른다. 판정 없이 "발행할 수 있습니다"라고 말하지 않는다.',
|
|
518
492
|
' 판정의 각 항목은 **왜 문제인지와 무엇을 바꾸면 되는지**를 함께 담고 있다. 그대로 전한다 — 요약하면 저작자가 어느 부품의 어느 축을 고칠지 다시 찾아야 한다.',
|
|
519
493
|
'- 무게·삼각형 수를 물으면 `getFigureSource` 의 `cost` 를 쓴다. 눈대중으로 세지 않는다.'
|