refactor(look-at): split tools.ts to comply with 200 LOC module rule
Extract input preparation and image conversion handling into look-at-input-preparer.ts. Extract prompt construction and multimodal session execution into look-at-prompt.ts and look-at-session-runner.ts while keeping createLookAt stable. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,154 @@
|
|||||||
|
import { basename } from "node:path"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
|
import type { LookAtArgs } from "./types"
|
||||||
|
import {
|
||||||
|
extractBase64Data,
|
||||||
|
inferMimeTypeFromBase64,
|
||||||
|
inferMimeTypeFromFilePath,
|
||||||
|
} from "./mime-type-inference"
|
||||||
|
import {
|
||||||
|
needsConversion,
|
||||||
|
convertImageToJpeg,
|
||||||
|
convertBase64ImageToJpeg,
|
||||||
|
cleanupConvertedImage,
|
||||||
|
} from "./image-converter"
|
||||||
|
import { log } from "../../shared"
|
||||||
|
|
||||||
|
export interface LookAtFilePart {
|
||||||
|
type: "file"
|
||||||
|
mime: string
|
||||||
|
url: string
|
||||||
|
filename: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PreparedLookAtInput {
|
||||||
|
readonly filePart: LookAtFilePart
|
||||||
|
readonly isBase64Input: boolean
|
||||||
|
readonly sourceDescription: string
|
||||||
|
cleanup(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrepareLookAtInputResult =
|
||||||
|
| { ok: true; value: PreparedLookAtInput }
|
||||||
|
| { ok: false; error: string }
|
||||||
|
|
||||||
|
function getTemporaryConversionPath(error: unknown): string | null {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const temporaryOutputPath = Reflect.get(error, "temporaryOutputPath")
|
||||||
|
if (typeof temporaryOutputPath === "string" && temporaryOutputPath.length > 0) {
|
||||||
|
return temporaryOutputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const temporaryDirectory = Reflect.get(error, "temporaryDirectory")
|
||||||
|
if (typeof temporaryDirectory === "string" && temporaryDirectory.length > 0) {
|
||||||
|
return temporaryDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareLookAtInput(args: LookAtArgs): PrepareLookAtInputResult {
|
||||||
|
const imageData = args.image_data
|
||||||
|
const filePath = args.file_path
|
||||||
|
|
||||||
|
if (imageData) {
|
||||||
|
const mimeType = inferMimeTypeFromBase64(imageData)
|
||||||
|
|
||||||
|
let finalBase64Data = extractBase64Data(imageData)
|
||||||
|
let finalMimeType = mimeType
|
||||||
|
let tempFilesToCleanup: string[] = []
|
||||||
|
|
||||||
|
if (needsConversion(mimeType)) {
|
||||||
|
log(`[look_at] Detected unsupported Base64 format: ${mimeType}, converting to JPEG...`)
|
||||||
|
try {
|
||||||
|
const { base64, tempFiles } = convertBase64ImageToJpeg(finalBase64Data, mimeType)
|
||||||
|
finalBase64Data = base64
|
||||||
|
finalMimeType = "image/jpeg"
|
||||||
|
tempFilesToCleanup = tempFiles
|
||||||
|
log("[look_at] Base64 conversion successful")
|
||||||
|
} catch (conversionError) {
|
||||||
|
log(`[look_at] Base64 conversion failed: ${conversionError}`)
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Error: Failed to convert Base64 image format. ${conversionError}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
isBase64Input: true,
|
||||||
|
sourceDescription: "clipboard/pasted image",
|
||||||
|
filePart: {
|
||||||
|
type: "file",
|
||||||
|
mime: finalMimeType,
|
||||||
|
url: `data:${finalMimeType};base64,${finalBase64Data}`,
|
||||||
|
filename: `clipboard-image.${finalMimeType.split("/")[1] || "png"}`,
|
||||||
|
},
|
||||||
|
cleanup() {
|
||||||
|
for (const temporaryFile of tempFilesToCleanup) {
|
||||||
|
cleanupConvertedImage(temporaryFile)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
let mimeType = inferMimeTypeFromFilePath(filePath)
|
||||||
|
let actualFilePath = filePath
|
||||||
|
let tempFilePath: string | null = null
|
||||||
|
let tempConversionPath: string | null = null
|
||||||
|
|
||||||
|
if (needsConversion(mimeType)) {
|
||||||
|
log(`[look_at] Detected unsupported format: ${mimeType}, converting to JPEG...`)
|
||||||
|
try {
|
||||||
|
tempFilePath = convertImageToJpeg(filePath, mimeType)
|
||||||
|
tempConversionPath = tempFilePath
|
||||||
|
actualFilePath = tempFilePath
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
log(`[look_at] Conversion successful: ${tempFilePath}`)
|
||||||
|
} catch (conversionError) {
|
||||||
|
const failedConversionPath = getTemporaryConversionPath(conversionError)
|
||||||
|
if (failedConversionPath) {
|
||||||
|
tempConversionPath = failedConversionPath
|
||||||
|
}
|
||||||
|
log(`[look_at] Conversion failed: ${conversionError}`)
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Error: Failed to convert image format. ${conversionError}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
isBase64Input: false,
|
||||||
|
sourceDescription: filePath,
|
||||||
|
filePart: {
|
||||||
|
type: "file",
|
||||||
|
mime: mimeType,
|
||||||
|
url: pathToFileURL(actualFilePath).href,
|
||||||
|
filename: basename(actualFilePath),
|
||||||
|
},
|
||||||
|
cleanup() {
|
||||||
|
if (tempConversionPath) {
|
||||||
|
cleanupConvertedImage(tempConversionPath)
|
||||||
|
} else if (tempFilePath) {
|
||||||
|
cleanupConvertedImage(tempFilePath)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Error: Must provide either 'file_path' or 'image_data'.",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const READ_ENABLED = false
|
||||||
|
|
||||||
|
export function buildLookAtPrompt(goal: string, isBase64Input: boolean): string {
|
||||||
|
const subjectNoun = isBase64Input ? "image" : "file"
|
||||||
|
const sourceClause = READ_ENABLED
|
||||||
|
? "Use the Read tool on the provided file path to load its contents, then analyze it."
|
||||||
|
: `The ${subjectNoun} is already attached to this message. Analyze it directly from the attachment. Do NOT attempt to use the Read tool. The Read tool is disabled for this invocation and the ${subjectNoun} cannot be loaded by path.`
|
||||||
|
|
||||||
|
return `Analyze the attached ${subjectNoun} and extract the requested information.
|
||||||
|
|
||||||
|
${sourceClause}
|
||||||
|
|
||||||
|
Goal: ${goal}
|
||||||
|
|
||||||
|
Provide ONLY the extracted information that matches the goal.
|
||||||
|
Be thorough on what was requested, concise on everything else.
|
||||||
|
If the requested information is not found, clearly state what is missing.`
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import type { ToolContext } from "@opencode-ai/plugin/tool"
|
||||||
|
import { log, promptSyncWithModelSuggestionRetry } from "../../shared"
|
||||||
|
import { extractLatestAssistantText } from "./assistant-message-extractor"
|
||||||
|
import { MULTIMODAL_LOOKER_AGENT } from "./constants"
|
||||||
|
import { READ_ENABLED, buildLookAtPrompt } from "./look-at-prompt"
|
||||||
|
import type { LookAtFilePart } from "./look-at-input-preparer"
|
||||||
|
import { resolveMultimodalLookerAgentMetadata } from "./multimodal-agent-metadata"
|
||||||
|
|
||||||
|
interface RunLookAtSessionInput {
|
||||||
|
ctx: PluginInput
|
||||||
|
toolContext: ToolContext
|
||||||
|
goal: string
|
||||||
|
filePart: LookAtFilePart
|
||||||
|
isBase64Input: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runLookAtSession({
|
||||||
|
ctx,
|
||||||
|
toolContext,
|
||||||
|
goal,
|
||||||
|
filePart,
|
||||||
|
isBase64Input,
|
||||||
|
}: RunLookAtSessionInput): Promise<string> {
|
||||||
|
const prompt = buildLookAtPrompt(goal, isBase64Input)
|
||||||
|
const { agentModel, agentVariant } = await resolveMultimodalLookerAgentMetadata(ctx)
|
||||||
|
|
||||||
|
log(`[look_at] Creating session with parent: ${toolContext.sessionID}`)
|
||||||
|
const parentSession = await ctx.client.session.get({
|
||||||
|
path: { id: toolContext.sessionID },
|
||||||
|
}).catch(() => null)
|
||||||
|
const parentDirectory = parentSession?.data?.directory ?? ctx.directory
|
||||||
|
|
||||||
|
const createResult = await ctx.client.session.create({
|
||||||
|
body: {
|
||||||
|
parentID: toolContext.sessionID,
|
||||||
|
title: `look_at: ${goal.substring(0, 50)}`,
|
||||||
|
},
|
||||||
|
query: { directory: parentDirectory },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (createResult.error) {
|
||||||
|
log("[look_at] Session create error:", createResult.error)
|
||||||
|
const errorString = String(createResult.error)
|
||||||
|
if (errorString.toLowerCase().includes("unauthorized")) {
|
||||||
|
return `Error: Failed to create session (Unauthorized). This may be due to:
|
||||||
|
1. OAuth token restrictions (e.g., Claude Code credentials are restricted to Claude Code only)
|
||||||
|
2. Provider authentication issues
|
||||||
|
3. Session permission inheritance problems
|
||||||
|
|
||||||
|
Try using a different provider or API key authentication.
|
||||||
|
|
||||||
|
Original error: ${createResult.error}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Error: Failed to create session: ${createResult.error}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionID = createResult.data.id
|
||||||
|
log(`[look_at] Created session: ${sessionID}`)
|
||||||
|
|
||||||
|
log(`[look_at] Sending prompt with ${isBase64Input ? "base64 image" : "file"} to session ${sessionID}`)
|
||||||
|
try {
|
||||||
|
await promptSyncWithModelSuggestionRetry(ctx.client, {
|
||||||
|
path: { id: sessionID },
|
||||||
|
body: {
|
||||||
|
agent: MULTIMODAL_LOOKER_AGENT,
|
||||||
|
tools: {
|
||||||
|
task: false,
|
||||||
|
call_omo_agent: false,
|
||||||
|
look_at: false,
|
||||||
|
read: READ_ENABLED,
|
||||||
|
},
|
||||||
|
parts: [
|
||||||
|
{ type: "text", text: prompt },
|
||||||
|
filePart,
|
||||||
|
],
|
||||||
|
...(agentModel ? { model: { providerID: agentModel.providerID, modelID: agentModel.modelID } } : {}),
|
||||||
|
...(agentVariant ? { variant: agentVariant } : {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (promptError) {
|
||||||
|
log("[look_at] Prompt error (ignored, will still fetch messages):", promptError)
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`[look_at] Fetching messages from session ${sessionID}...`)
|
||||||
|
const messagesResult = await ctx.client.session.messages({
|
||||||
|
path: { id: sessionID },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (messagesResult.error) {
|
||||||
|
log("[look_at] Messages error:", messagesResult.error)
|
||||||
|
return `Error: Failed to get messages: ${messagesResult.error}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = messagesResult.data
|
||||||
|
log(`[look_at] Got ${messages.length} messages`)
|
||||||
|
|
||||||
|
const responseText = extractLatestAssistantText(messages)
|
||||||
|
if (!responseText) {
|
||||||
|
log("[look_at] No assistant message found")
|
||||||
|
return "Error: No response from multimodal-looker agent"
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`[look_at] Got response, length: ${responseText.length}`)
|
||||||
|
return responseText
|
||||||
|
}
|
||||||
+18
-209
@@ -1,43 +1,11 @@
|
|||||||
import { basename } from "node:path"
|
|
||||||
import { pathToFileURL } from "node:url"
|
|
||||||
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
|
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
|
||||||
import { LOOK_AT_DESCRIPTION, MULTIMODAL_LOOKER_AGENT } from "./constants"
|
import { LOOK_AT_DESCRIPTION } from "./constants"
|
||||||
import type { LookAtArgs } from "./types"
|
import type { LookAtArgs } from "./types"
|
||||||
import { log, promptSyncWithModelSuggestionRetry } from "../../shared"
|
import { log } from "../../shared"
|
||||||
import { extractLatestAssistantText } from "./assistant-message-extractor"
|
|
||||||
import type { LookAtArgsWithAlias } from "./look-at-arguments"
|
import type { LookAtArgsWithAlias } from "./look-at-arguments"
|
||||||
import { normalizeArgs, validateArgs } from "./look-at-arguments"
|
import { normalizeArgs, validateArgs } from "./look-at-arguments"
|
||||||
import {
|
import { prepareLookAtInput } from "./look-at-input-preparer"
|
||||||
extractBase64Data,
|
import { runLookAtSession } from "./look-at-session-runner"
|
||||||
inferMimeTypeFromBase64,
|
|
||||||
inferMimeTypeFromFilePath,
|
|
||||||
} from "./mime-type-inference"
|
|
||||||
import { resolveMultimodalLookerAgentMetadata } from "./multimodal-agent-metadata"
|
|
||||||
import {
|
|
||||||
needsConversion,
|
|
||||||
convertImageToJpeg,
|
|
||||||
convertBase64ImageToJpeg,
|
|
||||||
cleanupConvertedImage,
|
|
||||||
} from "./image-converter"
|
|
||||||
|
|
||||||
function getTemporaryConversionPath(error: unknown): string | null {
|
|
||||||
if (!(error instanceof Error)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const temporaryOutputPath = Reflect.get(error, "temporaryOutputPath")
|
|
||||||
if (typeof temporaryOutputPath === "string" && temporaryOutputPath.length > 0) {
|
|
||||||
return temporaryOutputPath
|
|
||||||
}
|
|
||||||
|
|
||||||
const temporaryDirectory = Reflect.get(error, "temporaryDirectory")
|
|
||||||
if (typeof temporaryDirectory === "string" && temporaryDirectory.length > 0) {
|
|
||||||
return temporaryDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export { normalizeArgs, validateArgs } from "./look-at-arguments"
|
export { normalizeArgs, validateArgs } from "./look-at-arguments"
|
||||||
|
|
||||||
@@ -57,188 +25,29 @@ export function createLookAt(ctx: PluginInput): ToolDefinition {
|
|||||||
return validationError
|
return validationError
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBase64Input = Boolean(args.image_data)
|
const preparedInputResult = prepareLookAtInput(args)
|
||||||
const sourceDescription = isBase64Input ? "clipboard/pasted image" : args.file_path
|
if (!preparedInputResult.ok) {
|
||||||
|
return preparedInputResult.error
|
||||||
|
}
|
||||||
|
|
||||||
|
const preparedInput = preparedInputResult.value
|
||||||
|
const { isBase64Input, sourceDescription } = preparedInput
|
||||||
log(`[look_at] Analyzing ${sourceDescription}, goal: ${args.goal}`)
|
log(`[look_at] Analyzing ${sourceDescription}, goal: ${args.goal}`)
|
||||||
|
|
||||||
const imageData = args.image_data
|
|
||||||
const filePath = args.file_path
|
|
||||||
|
|
||||||
let mimeType: string
|
|
||||||
let filePart: { type: "file"; mime: string; url: string; filename: string }
|
|
||||||
let tempFilePath: string | null = null
|
|
||||||
let tempConversionPath: string | null = null
|
|
||||||
let tempFilesToCleanup: string[] = []
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (imageData) {
|
return await runLookAtSession({
|
||||||
mimeType = inferMimeTypeFromBase64(imageData)
|
ctx,
|
||||||
|
toolContext,
|
||||||
let finalBase64Data = extractBase64Data(imageData)
|
goal: args.goal,
|
||||||
let finalMimeType = mimeType
|
filePart: preparedInput.filePart,
|
||||||
|
isBase64Input,
|
||||||
if (needsConversion(mimeType)) {
|
|
||||||
log(`[look_at] Detected unsupported Base64 format: ${mimeType}, converting to JPEG...`)
|
|
||||||
try {
|
|
||||||
const { base64, tempFiles } = convertBase64ImageToJpeg(finalBase64Data, mimeType)
|
|
||||||
finalBase64Data = base64
|
|
||||||
finalMimeType = "image/jpeg"
|
|
||||||
tempFilesToCleanup = tempFiles
|
|
||||||
log(`[look_at] Base64 conversion successful`)
|
|
||||||
} catch (conversionError) {
|
|
||||||
log(`[look_at] Base64 conversion failed: ${conversionError}`)
|
|
||||||
return `Error: Failed to convert Base64 image format. ${conversionError}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filePart = {
|
|
||||||
type: "file",
|
|
||||||
mime: finalMimeType,
|
|
||||||
url: `data:${finalMimeType};base64,${finalBase64Data}`,
|
|
||||||
filename: `clipboard-image.${finalMimeType.split("/")[1] || "png"}`,
|
|
||||||
}
|
|
||||||
} else if (filePath) {
|
|
||||||
mimeType = inferMimeTypeFromFilePath(filePath)
|
|
||||||
|
|
||||||
let actualFilePath = filePath
|
|
||||||
if (needsConversion(mimeType)) {
|
|
||||||
log(`[look_at] Detected unsupported format: ${mimeType}, converting to JPEG...`)
|
|
||||||
try {
|
|
||||||
tempFilePath = convertImageToJpeg(filePath, mimeType)
|
|
||||||
tempConversionPath = tempFilePath
|
|
||||||
actualFilePath = tempFilePath
|
|
||||||
mimeType = "image/jpeg"
|
|
||||||
log(`[look_at] Conversion successful: ${tempFilePath}`)
|
|
||||||
} catch (conversionError) {
|
|
||||||
const failedConversionPath = getTemporaryConversionPath(conversionError)
|
|
||||||
if (failedConversionPath) {
|
|
||||||
tempConversionPath = failedConversionPath
|
|
||||||
}
|
|
||||||
log(`[look_at] Conversion failed: ${conversionError}`)
|
|
||||||
return `Error: Failed to convert image format. ${conversionError}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filePart = {
|
|
||||||
type: "file",
|
|
||||||
mime: mimeType,
|
|
||||||
url: pathToFileURL(actualFilePath).href,
|
|
||||||
filename: basename(actualFilePath),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return "Error: Must provide either 'file_path' or 'image_data'."
|
|
||||||
}
|
|
||||||
|
|
||||||
const readEnabled = false
|
|
||||||
const subjectNoun = isBase64Input ? "image" : "file"
|
|
||||||
const sourceClause = readEnabled
|
|
||||||
? `Use the Read tool on the provided file path to load its contents, then analyze it.`
|
|
||||||
: `The ${subjectNoun} is already attached to this message. Analyze it directly from the attachment. Do NOT attempt to use the Read tool. The Read tool is disabled for this invocation and the ${subjectNoun} cannot be loaded by path.`
|
|
||||||
|
|
||||||
const prompt = `Analyze the attached ${subjectNoun} and extract the requested information.
|
|
||||||
|
|
||||||
${sourceClause}
|
|
||||||
|
|
||||||
Goal: ${args.goal}
|
|
||||||
|
|
||||||
Provide ONLY the extracted information that matches the goal.
|
|
||||||
Be thorough on what was requested, concise on everything else.
|
|
||||||
If the requested information is not found, clearly state what is missing.`
|
|
||||||
|
|
||||||
const { agentModel, agentVariant } = await resolveMultimodalLookerAgentMetadata(ctx)
|
|
||||||
|
|
||||||
log(`[look_at] Creating session with parent: ${toolContext.sessionID}`)
|
|
||||||
const parentSession = await ctx.client.session.get({
|
|
||||||
path: { id: toolContext.sessionID },
|
|
||||||
}).catch(() => null)
|
|
||||||
const parentDirectory = parentSession?.data?.directory ?? ctx.directory
|
|
||||||
|
|
||||||
const createResult = await ctx.client.session.create({
|
|
||||||
body: {
|
|
||||||
parentID: toolContext.sessionID,
|
|
||||||
title: `look_at: ${args.goal.substring(0, 50)}`,
|
|
||||||
},
|
|
||||||
query: { directory: parentDirectory },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (createResult.error) {
|
|
||||||
log(`[look_at] Session create error:`, createResult.error)
|
|
||||||
const errorStr = String(createResult.error)
|
|
||||||
if (errorStr.toLowerCase().includes("unauthorized")) {
|
|
||||||
return `Error: Failed to create session (Unauthorized). This may be due to:
|
|
||||||
1. OAuth token restrictions (e.g., Claude Code credentials are restricted to Claude Code only)
|
|
||||||
2. Provider authentication issues
|
|
||||||
3. Session permission inheritance problems
|
|
||||||
|
|
||||||
Try using a different provider or API key authentication.
|
|
||||||
|
|
||||||
Original error: ${createResult.error}`
|
|
||||||
}
|
|
||||||
return `Error: Failed to create session: ${createResult.error}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionID = createResult.data.id
|
|
||||||
log(`[look_at] Created session: ${sessionID}`)
|
|
||||||
|
|
||||||
log(`[look_at] Sending prompt with ${isBase64Input ? "base64 image" : "file"} to session ${sessionID}`)
|
|
||||||
try {
|
|
||||||
await promptSyncWithModelSuggestionRetry(ctx.client, {
|
|
||||||
path: { id: sessionID },
|
|
||||||
body: {
|
|
||||||
agent: MULTIMODAL_LOOKER_AGENT,
|
|
||||||
tools: {
|
|
||||||
task: false,
|
|
||||||
call_omo_agent: false,
|
|
||||||
look_at: false,
|
|
||||||
read: readEnabled,
|
|
||||||
},
|
|
||||||
parts: [
|
|
||||||
{ type: "text", text: prompt },
|
|
||||||
filePart,
|
|
||||||
],
|
|
||||||
...(agentModel ? { model: { providerID: agentModel.providerID, modelID: agentModel.modelID } } : {}),
|
|
||||||
...(agentVariant ? { variant: agentVariant } : {}),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
} catch (promptError) {
|
|
||||||
log(`[look_at] Prompt error (ignored, will still fetch messages):`, promptError)
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`[look_at] Fetching messages from session ${sessionID}...`)
|
|
||||||
|
|
||||||
const messagesResult = await ctx.client.session.messages({
|
|
||||||
path: { id: sessionID },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (messagesResult.error) {
|
|
||||||
log(`[look_at] Messages error:`, messagesResult.error)
|
|
||||||
return `Error: Failed to get messages: ${messagesResult.error}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = messagesResult.data
|
|
||||||
log(`[look_at] Got ${messages.length} messages`)
|
|
||||||
|
|
||||||
const responseText = extractLatestAssistantText(messages)
|
|
||||||
if (!responseText) {
|
|
||||||
log("[look_at] No assistant message found")
|
|
||||||
return "Error: No response from multimodal-looker agent"
|
|
||||||
}
|
|
||||||
|
|
||||||
log(`[look_at] Got response, length: ${responseText.length}`)
|
|
||||||
return responseText
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
log(`[look_at] Unexpected error analyzing ${sourceDescription}:`, error)
|
log(`[look_at] Unexpected error analyzing ${sourceDescription}:`, error)
|
||||||
return `Error: Failed to analyze ${sourceDescription}: ${errorMessage}`
|
return `Error: Failed to analyze ${sourceDescription}: ${errorMessage}`
|
||||||
} finally {
|
} finally {
|
||||||
if (tempConversionPath) {
|
preparedInput.cleanup()
|
||||||
cleanupConvertedImage(tempConversionPath)
|
|
||||||
} else if (tempFilePath) {
|
|
||||||
cleanupConvertedImage(tempFilePath)
|
|
||||||
}
|
|
||||||
tempFilesToCleanup.forEach(file => {
|
|
||||||
cleanupConvertedImage(file)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user