Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
58 changes: 57 additions & 1 deletion frontend/src/api/adk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ export type ADKResponsePart = {
"requestedAuthConfigs": object,
"transferToAgent": string|undefined
},
"errorCode"?: string,
"errorMessage"?: string,
"id": string,
"timestamp": number
}
Expand Down Expand Up @@ -209,20 +211,57 @@ export const sseRequest = async (
}


/**
* Checks if an ADK response part contains an error using proper ADK error fields
* @param responsePart - A single ADK response part to check for errors
* @returns ManugenError if error is found, null otherwise
*/
export const checkForADKError = (responsePart: ADKResponsePart): ManugenError | null => {
if (responsePart.errorCode && responsePart.errorMessage) {
try {
// Try to parse the error message as JSON to get structured error data
const errorData = JSON.parse(responsePart.errorMessage);
return new ManugenError(
errorData.error_type || responsePart.errorCode,
errorData.message || 'An error occurred',
errorData.details || '',
errorData.suggestion || ''
);
} catch {
// If JSON parsing fails, create error from the raw error fields
return new ManugenError(
responsePart.errorCode,
responsePart.errorMessage,
'',
''
);
}
}
return null;
};

/**
* Utility method to extract 'text' sections from an ADK API response
*
* @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
if (!response) {
return "";
}

// Check each response part for errors using proper ADK error fields
for (const responsePart of response) {
const error = checkForADKError(responsePart);
if (error) {
throw error;
}
}

const textSections = response
.filter(item => item.content && item.content.parts)
.map(item => item.content.parts)
Expand All @@ -244,3 +283,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;
}
}
20 changes: 12 additions & 8 deletions frontend/src/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { toast, type ToastOptions } from 'vue3-toastify';
import { api, request } from "./";

import {
ensureSessionExists, extractADKText, sseRequest,
ensureSessionExists, extractADKText, sseRequest, checkForADKError,
type ADKResponse, type ADKResponsePart, type ADKSessionResponse
} from "./adk";

Expand Down Expand Up @@ -115,13 +115,17 @@ export const aiWriterAsync = async (input: string, session: ADKSessionResponse|n

console.log("Final event log received:", eventLog);

toast("Done!", {
position: "bottom-left",
autoClose: 6000,
hideProgressBar: true,
type: "success",
transition: "bounce",
} as ToastOptions);
// Only show success toast if no errors occurred
const hasErrors = eventLog.some(event => event.errorCode && event.errorMessage);
if (!hasErrors) {
toast("Done!", {
position: "bottom-left",
autoClose: 6000,
hideProgressBar: true,
type: "success",
transition: "bounce",
} as ToastOptions);
}

return eventLog;
}
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
112 changes: 91 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 @@ -268,6 +269,8 @@ const getContext = () => {

/** selected text */
sel: doc.textBetween(from, to, ""),
/** selected content as JSON (preserves structure) */
selJSON: doc.slice(from, to).content.toJSON(),
/** current paragraph text */
selP: p?.textContent ?? "",

Expand Down Expand Up @@ -311,6 +314,62 @@ const findPortal = (id: string) => {
return findChildren(doc, (node) => node.attrs.id === id)?.[0];
};

/** helper function to handle error display and editor content replacement */
const handleActionError = (error: any, portalId: string, originalContent?: any) => {
/** find node of portal created earlier */
const portalNode = findPortal(portalId);
if (!portalNode) return;

let errorContent: any;
let toastMessage: string;
let toastDuration: number;

// Handle ManugenError specially
if (error instanceof ManugenError) {
// Show a detailed error toast
toastMessage = `<strong>${error.message}</strong><br/>${error.suggestion ? `<em>Suggestion: ${error.suggestion}</em>` : ''}`;
toastDuration = 10000;

// Restore original content if available, otherwise show error message
if (originalContent) {
errorContent = originalContent;
} else {
errorContent = paragraphizeToJSON(`⚠️ Error: ${error.message}${error.suggestion ? `\n\nSuggestion: ${error.suggestion}` : ''}`);
}
} else {
// Handle generic errors
console.error('Unexpected error in action:', error);
toastMessage = 'An unexpected error occurred. Please try again.';
toastDuration = 5000;

// Restore original content if available, otherwise show generic error message
errorContent = originalContent || paragraphizeToJSON('⚠️ An unexpected error occurred. Please try again.');
}

// Show error toast
toast.error(toastMessage, {
position: "bottom-left",
autoClose: toastDuration,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
dangerouslyHTMLString: true,
});

// Update editor content
if (!editor.value) return;

editor.value
.chain()
/** delete portal node */
.deleteRange({
from: portalNode.pos,
to: portalNode.pos + portalNode.node.nodeSize,
})
.insertContentAt(portalNode.pos, errorContent)
.run();
};

/** create func that runs an action and handles placeholder in the editor while its working */
const action =
(
Expand All @@ -326,32 +385,43 @@ const action =
const context = getContext();
if (!context) return;

// Store original content for potential restoration on error
const originalContent = context.selJSON;

/** create portal */
const portalId = addPortal();
if (!portalId) return;

/** 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];

// Use helper function to handle error display and editor update
handleActionError(error, portalId, originalContent);
}
};

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
)

# Use proper ADK error fields instead of sentinel strings
return Event(
author=self.name,
invocation_id=ctx.invocation_id,
error_code=error_type,
error_message=error_response.model_dump_json(),
content=types.Content(
role="model",
parts=[
types.Part(text=f"Error: {message}"),
],
),
)

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