PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.4
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.4
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / src / utils / processAgentResponse.js

processAgentResponse.js in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.4, at src/utils/processAgentResponse.js

166 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Process agent response into a structured result (pure function).
3 *
4 * Extracts assistant message, execution results, and session updates
5 * from the raw API response. Keeps side-effect dispatch in the hook.
6 *
7 * @param {Object} response - Raw API response
8 * @param {Object} flags - Stream-time flags
9 * @param {boolean} flags.hasStreamedComponents - Whether components were already rendered in real-time
10 * @returns {Object} Structured result for the hook to dispatch
11 */
12
13 /**
14 * Strip framework debug noise from the raw message text.
15 * Removes patterns like "(Empty response: {'content': [], ...})" that leak
16 * from the LLM framework when a step returns empty content.
17 */
18 export const sanitizeMessageText = (text) => {
19 if (!text) return '';
20 return text.replace(/\s*\(Empty response:[\s\S]*$/, '').trim();
21 };
22
23 const buildTextBlock = (text, { minimized = false } = {}) => ({
24 type: 'text',
25 text,
26 ...(minimized ? { minimized: true } : {}),
27 });
28
29 const buildComponentBlocks = (response) => {
30 const components = Array.isArray(response?.messageComponents) ? response.messageComponents : [];
31
32 return components
33 .filter((component) => component?.componentType)
34 .map((component) => ({
35 type: 'component',
36 componentType: component.componentType,
37 props: component.props || {},
38 }));
39 };
40
41 const buildBrainStateBlock = (response) => {
42 if (!response?.brain_state && !response?.plan_state && !response?.verification_status) {
43 return null;
44 }
45
46 return {
47 type: 'brain_state',
48 brainState: response.brain_state || null,
49 planState: response.plan_state || null,
50 verificationStatus: response.verification_status || null,
51 };
52 };
53
54 const buildAssistantContent = ({
55 messageContent,
56 componentBlocks,
57 brainStateBlock,
58 hasStreamedComponents,
59 }) => {
60 const blocks = [];
61
62 // Always persist post-turn text — pre-component text is already merged
63 // into the component message during streaming (chatActions.js).
64 if (messageContent) {
65 blocks.push(buildTextBlock(messageContent));
66 }
67
68 if (!hasStreamedComponents && componentBlocks.length > 0) {
69 blocks.push(...componentBlocks);
70 }
71
72 if (brainStateBlock) {
73 blocks.push(brainStateBlock);
74 }
75
76 if (blocks.length === 0) {
77 return null;
78 }
79
80 if (
81 blocks.length === 1 &&
82 blocks[0].type === 'text'
83 ) {
84 return blocks[0].text;
85 }
86
87 return blocks;
88 };
89
90 export const processAgentResponse = (
91 response,
92 {
93 hasStreamedComponents,
94 thinkingText = '',
95 hasTextDeltas = true,
96 toolCalls,
97 }
98 ) => {
99 const result = {
100 sessionId: response?.sessionId || response?.session_id || null,
101 assistantMessage: null,
102 jsHookResults: [],
103 pageCreationResults: [],
104 };
105
106 const componentBlocks = buildComponentBlocks(response);
107 const brainStateBlock = buildBrainStateBlock(response);
108 const hasTextResponse = !!(response?.message || response?.response);
109 const hasComponentResponse = componentBlocks.length > 0;
110 const isThinkingSourced = !hasTextDeltas && !!thinkingText;
111
112 if (hasTextResponse || hasComponentResponse || isThinkingSourced || brainStateBlock) {
113 let messageContent = sanitizeMessageText(response?.message || response?.response || '');
114
115 if (response?.generatedImages?.length > 0) {
116 const imageMarkdown = response.generatedImages
117 .map((img) => `![Generated Image](${img.url})`)
118 .join('\n\n');
119 messageContent = `${messageContent}\n\n${imageMarkdown}`;
120 }
121
122 const content = buildAssistantContent({
123 messageContent,
124 componentBlocks,
125 brainStateBlock,
126 hasStreamedComponents,
127 });
128
129 if (isThinkingSourced) {
130 result.assistantMessage = {
131 role: 'assistant',
132 content: content || '',
133 thinkingText,
134 };
135 } else if (content) {
136 result.assistantMessage = {
137 role: 'assistant',
138 content,
139 };
140 }
141 }
142
143 // Attach toolCalls to assistant message
144 if (result.assistantMessage && toolCalls && toolCalls.length > 0) {
145 result.assistantMessage.toolCalls = toolCalls;
146 }
147
148 if (response?.execution_results?.length > 0) {
149 result.jsHookResults = response.execution_results.filter((r) =>
150 r.execution_mode === 'js_hook' || r.execution_mode === 'hybrid'
151 );
152
153 result.pageCreationResults = response.execution_results.filter(
154 (r) => r.execution_mode === 'rest_api' && r.edit_url
155 );
156
157 // Extract batch_updates from tool results
158 const batchUpdates = response.execution_results?.filter(r => r.batch_updates)?.map(r => r.batch_updates) || [];
159 if (batchUpdates.length > 0 && result.assistantMessage) {
160 result.assistantMessage.batch_updates = batchUpdates;
161 }
162 }
163
164 return result;
165 };
166