Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,196 changes: 5,196 additions & 0 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@tsconfig/node22": "^22.0.1",
"@types/lodash": "^4.17.17",
"@types/node": "^22.14.0",
"@types/showdown": "^2.0.6",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^7.0.2",
Expand Down
55 changes: 54 additions & 1 deletion frontend/src/api/adk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export const sseRequest = async (
* @param response - The ADK API response to extract text from
* @param onlyLast - If true, returns only the last text section; if false,
* returns all text sections concatenated with newlines
* @returns The extracted text as a string
* @returns The extracted text as a string, or throws an error if error response detected
*/
export const extractADKText = (response: ADKResponse|undefined, onlyLast: boolean = true): string => {
// if response is undefined, return an empty string
Expand All @@ -235,6 +235,42 @@ export const extractADKText = (response: ADKResponse|undefined, onlyLast: boolea
return textParts.map(x => x.text).join("\n");
})

// Check for structured error responses
const allText = textSections.join("\n");
if (allText.includes("MANUGEN_ERROR:")) {
const errorMatch = allText.match(/MANUGEN_ERROR:\s*(\{.*?\})/s);
if (errorMatch) {
try {
const errorData = JSON.parse(errorMatch[1]);
throw new ManugenError(
errorData.error_type || 'unknown_error',
errorData.message || 'An error occurred',
errorData.details || '',
errorData.suggestion || ''
);
} catch (e) {
if (e instanceof ManugenError) {
throw e;
}
// If JSON parsing fails, throw a generic error
throw new ManugenError(
'parse_error',
'Failed to parse error response',
'The server returned an error but it could not be parsed properly.',
'Please try again or contact support if the problem persists.'
);
}
} else {
// MANUGEN_ERROR found but no valid JSON match
throw new ManugenError(
'parse_error',
'Malformed error response detected',
'The server returned an error but it could not be parsed properly.',
'Please try again or contact support if the problem persists.'
);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, I dislike it looking for a sentinel string to detect an error. This code will also erroneously report an error if "MANUGEN_ERROR:" occurs anywhere in the text. IMO if you were going to use sentinel strings for errors, you should perform this check over each textPart and see if it begins with MANUGEN_ERROR:.

Even better, in the server-sent events case, this error-detecting code should be moved to the server-side events handler, so that errors are displayed immediately rather than once the entire response has been received.

Regarding how errors are flagged, you could add the error_code and error_message fields to the event to make it a proper ADK error; this code would then look for those fields and report them.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed all the issues you mentioned:

  1. Removed sentinel strings: Now using proper ADK error_code and error_message fields
  2. Early error detection: Created checkForADKError() function that checks each response part individually
  3. Immediate error handling: The extractADKText() function now checks for errors early in the response processing rather than after concatenating all text
  4. Proper ADK error structure: Using the structured error fields as you suggested

The error detection now happens immediately when processing each ADKResponsePart, eliminating the risk of false positives and providing much faster feedback to users. (2b78cd8)

if (onlyLast) {
// if onlyLast is true, return the last text section
return textSections.length > 0 ? textSections[textSections.length - 1] : "";
Expand All @@ -244,3 +280,20 @@ export const extractADKText = (response: ADKResponse|undefined, onlyLast: boolea
return textSections.join("\n");
}
}

/**
* Custom error class for Manugen AI errors
*/
export class ManugenError extends Error {
public readonly errorType: string;
public readonly details: string;
public readonly suggestion: string;

constructor(errorType: string, message: string, details: string = '', suggestion: string = '') {
super(message);
this.name = 'ManugenError';
this.errorType = errorType;
this.details = details;
this.suggestion = suggestion;
}
}
2 changes: 1 addition & 1 deletion frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const request = async <Response>(
if (!response.ok) error = "Response not OK";

/** try to parse as json */
let parsed: Response;
let parsed: Response | undefined;
try {
parsed = await response.clone().json();
} catch (e) {
Expand Down
105 changes: 84 additions & 21 deletions frontend/src/pages/PageEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ import Portal from "./portal";
import { agentsWorking } from "@/components/AppAgents.vue";
import type { AgentId } from "@/api/agents";
import { aiWriter, aiWriterAsync } from "@/api/endpoints";
import { type ADKSessionResponse, ensureSessionExists, extractADKText } from "@/api/adk";
import { type ADKSessionResponse, ensureSessionExists, extractADKText, ManugenError } from "@/api/adk";
import example from "./example.txt?raw";
import { toast } from 'vue3-toastify';

/** app info */
const { VITE_TITLE: title } = import.meta.env;
Expand All @@ -170,7 +171,7 @@ onMounted(() => {
adkUsername.value,
adkSessionId.value
).then((data) => {
sessionData.value = data;
sessionData.value = data as ADKSessionResponse;
console.log("ADK session created:", data);
}).catch((error) => {
console.error("Error creating ADK session:", error);
Expand Down Expand Up @@ -333,25 +334,87 @@ const action =
/** tell agents component that these agents are working in this portal */
agentsWorking.value[portalId] = agents;

/** run actual work func, providing context */
const result = await func(context);

/** tell agents component that work is done */
delete agentsWorking.value[portalId];

/** find node of portal created earlier */
const portalNode = findPortal(portalId);
if (!portalNode) return;

editor.value
.chain()
/** delete portal node */
.deleteRange({
from: portalNode.pos,
to: portalNode.pos + portalNode.node.nodeSize,
})
.insertContentAt(portalNode.pos, paragraphizeToJSON(result))
.run();
try {
/** run actual work func, providing context */
const result = await func(context);

/** tell agents component that work is done */
delete agentsWorking.value[portalId];

/** find node of portal created earlier */
const portalNode = findPortal(portalId);
if (!portalNode) return;

editor.value
.chain()
/** delete portal node */
.deleteRange({
from: portalNode.pos,
to: portalNode.pos + portalNode.node.nodeSize,
})
.insertContentAt(portalNode.pos, paragraphizeToJSON(result))
.run();
} catch (error) {
/** tell agents component that work is done */
delete agentsWorking.value[portalId];

/** find node of portal created earlier */
const portalNode = findPortal(portalId);
if (!portalNode) return;

// Handle ManugenError specially
if (error instanceof ManugenError) {
// Show a detailed error toast
toast.error(
`<strong>${error.message}</strong><br/>${error.suggestion ? `<em>Suggestion: ${error.suggestion}</em>` : ''}`,
{
position: "bottom-left",
autoClose: 10000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
dangerouslyHTMLString: true,
}
);

// Insert an error message in the editor instead of empty content
const errorText = `⚠️ Error: ${error.message}${error.suggestion ? `\n\nSuggestion: ${error.suggestion}` : ''}`;

editor.value
.chain()
/** delete portal node */
.deleteRange({
from: portalNode.pos,
to: portalNode.pos + portalNode.node.nodeSize,
})
.insertContentAt(portalNode.pos, paragraphizeToJSON(errorText))
.run();
} else {
// Handle generic errors
console.error('Unexpected error in action:', error);

toast.error(
'An unexpected error occurred. Please try again.',
{
position: "bottom-left",
autoClose: 5000,
}
);

// Insert a generic error message
const errorText = '⚠️ An unexpected error occurred. Please try again.';

editor.value
.chain()
/** delete portal node */
.deleteRange({
from: portalNode.pos,
to: portalNode.pos + portalNode.node.nodeSize,
})
.insertContentAt(portalNode.pos, paragraphizeToJSON(errorText))
.run();
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love that it essentially duplicated the same block of code for handling a specific and nonspecific error. I also don't love that it's emitting the error text into the document rather than doing something more useful, like reinserting the original text.

I'm not going to fix this since it'll likely get replaced in the upcoming UI refactor, but I thought I'd note it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the code duplication by creating a reusable handleActionError() helper function. Also improved the user experience - now restores the original selected text instead of inserting error messages into the document, as suggested. (2b78cd8)

}
};

const aiWriterSelectAction = (label: string, icon: any, prefix: string = "", agent: AgentId = "aiWriter") => {
Expand Down
44 changes: 43 additions & 1 deletion packages/manugen-ai/src/manugen_ai/adk.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import json
from abc import ABCMeta

from google.adk.agents import BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.events import Event, EventActions
from google.genai import types

from .schema import ErrorResponse


class ManugenAIBaseAgent(BaseAgent, metaclass=ABCMeta):
"""
TODO: add docs
Base agent class for Manugen AI with enhanced error handling.
"""

def error_message(self, ctx: InvocationContext, error_msg: str) -> Event:
Expand All @@ -26,6 +29,45 @@ def error_message(self, ctx: InvocationContext, error_msg: str) -> Event:
),
)

def structured_error_message(
self,
ctx: InvocationContext,
error_type: str,
message: str,
details: str = "",
suggestion: str = ""
) -> Event:
"""
Create a structured error response that the UI can parse and display properly.

Args:
ctx: The invocation context
error_type: Type of error (e.g., 'model_error', 'agent_error', 'validation_error')
message: Human-readable error message
details: Additional error details for debugging
suggestion: Suggested action for the user
"""
error_response = ErrorResponse(
error_type=error_type,
message=message,
details=details,
suggestion=suggestion
)

# Create a structured JSON response that the frontend can parse
error_json = error_response.model_dump_json()

return Event(
author=self.name,
invocation_id=ctx.invocation_id,
content=types.Content(
role="model",
parts=[
types.Part(text=f"MANUGEN_ERROR: {error_json}"),
],
),
)
Comment on lines +60 to +69
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get why it's sending the error back as part of the event stream; I suppose it's the least invasive way to add reporting from the agents through to the frontend.

While you can use ADK events to represent errors, the way it's done here only flags it as an error by the presence of a string, "MANUGEN_ERROR: ", preceding the error text. The ADK has a way to flag events as errors, documented here: https://google.github.io/adk-docs/events/#additional-context-and-event-details.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use proper ADK error fields! Now using error_code and error_message fields on the Event object instead of the sentinel string approach. This eliminates the risk of false positive errors from regular text and follows the ADK standard. (2b78cd8)


def get_transfer_to_agent_event(
self, ctx: InvocationContext, agent: BaseAgent
) -> Event:
Expand Down
Loading
Loading