From 67aa019489690b266c0f8373a993731f47ac196e Mon Sep 17 00:00:00 2001 From: Lily Delalande <119957291+lily-de@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:55:23 -0400 Subject: [PATCH] context_management: handle summarization in UI (#2377) --- crates/goose-server/src/openapi.rs | 28 + crates/goose-server/src/routes/context.rs | 39 +- crates/goose/src/message.rs | 18 +- crates/mcp-core/src/content.rs | 13 +- crates/mcp-core/src/handler.rs | 15 + crates/mcp-core/src/resource.rs | 5 +- crates/mcp-core/src/role.rs | 3 +- ui/desktop/openapi.json | 643 +++++++++++++++++- ui/desktop/src/api/sdk.gen.ts | 13 +- ui/desktop/src/api/types.gen.ts | 182 +++++ ui/desktop/src/components/ChatView.tsx | 148 ++-- .../ContextLengthExceededHandler.tsx | 77 +++ .../context_management/ContextManager.tsx | 170 +++++ .../components/context_management/index.ts | 142 +++- .../context_management/sessionManagement.ts | 0 ui/desktop/src/hooks/useMessageStream.ts | 24 +- ui/desktop/src/types/message.ts | 2 + 17 files changed, 1395 insertions(+), 127 deletions(-) create mode 100644 ui/desktop/src/components/context_management/ContextLengthExceededHandler.tsx create mode 100644 ui/desktop/src/components/context_management/ContextManager.tsx create mode 100644 ui/desktop/src/components/context_management/sessionManagement.ts diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index d8064c3cd0..e963dc370c 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -3,8 +3,16 @@ use goose::agents::extension::ToolInfo; use goose::agents::ExtensionConfig; use goose::config::permission::PermissionLevel; use goose::config::ExtensionEntry; +use goose::message::{ + ContextLengthExceeded, FrontendToolRequest, Message, MessageContent, RedactedThinkingContent, + ThinkingContent, ToolConfirmationRequest, ToolRequest, ToolResponse, +}; use goose::permission::permission_confirmation::PrincipalType; use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata}; +use mcp_core::content::{Annotations, Content, EmbeddedResource, ImageContent, TextContent}; +use mcp_core::handler::ToolResultSchema; +use mcp_core::resource::ResourceContents; +use mcp_core::role::Role; use mcp_core::tool::{Tool, ToolAnnotations}; use utoipa::OpenApi; @@ -25,6 +33,7 @@ use utoipa::OpenApi; super::routes::config_management::upsert_permissions, super::routes::agent::get_tools, super::routes::reply::confirm_permission, + super::routes::context::manage_context, // Added this path ), components(schemas( super::routes::config_management::UpsertConfigQuery, @@ -37,6 +46,25 @@ use utoipa::OpenApi; super::routes::config_management::ToolPermission, super::routes::config_management::UpsertPermissionsQuery, super::routes::reply::PermissionConfirmationRequest, + super::routes::context::ContextManageRequest, + super::routes::context::ContextManageResponse, + Message, + MessageContent, + Content, + EmbeddedResource, + ImageContent, + Annotations, + TextContent, + ToolResponse, + ToolRequest, + ToolResultSchema, + ToolConfirmationRequest, + ThinkingContent, + RedactedThinkingContent, + FrontendToolRequest, + ResourceContents, + ContextLengthExceeded, + Role, ProviderMetadata, ExtensionEntry, ExtensionConfig, diff --git a/crates/goose-server/src/routes/context.rs b/crates/goose-server/src/routes/context.rs index 46a526d0bf..0630b60721 100644 --- a/crates/goose-server/src/routes/context.rs +++ b/crates/goose-server/src/routes/context.rs @@ -9,23 +9,43 @@ use axum::{ use goose::message::Message; use serde::{Deserialize, Serialize}; use std::sync::Arc; +use utoipa::ToSchema; -// Direct message serialization for context mgmt request -#[derive(Debug, Deserialize)] +/// Request payload for context management operations +#[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContextManageRequest { - messages: Vec, - manage_action: String, + /// Collection of messages to be managed + pub messages: Vec, + /// Operation to perform: "truncation" or "summarize" + pub manage_action: String, } -// Direct message serialization for context mgmt request -#[derive(Debug, Serialize)] +/// Response from context management operations +#[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContextManageResponse { - messages: Vec, - token_counts: Vec, + /// Processed messages after the operation + pub messages: Vec, + /// Token counts for each processed message + pub token_counts: Vec, } +#[utoipa::path( + post, + path = "/context/manage", + request_body = ContextManageRequest, + responses( + (status = 200, description = "Context managed successfully", body = ContextManageResponse), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 412, description = "Precondition failed - Agent not available"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Context Management" +)] async fn manage_context( State(state): State>, headers: HeaderMap, @@ -40,7 +60,8 @@ async fn manage_context( let mut processed_messages: Vec = vec![]; let mut token_counts: Vec = vec![]; - if request.manage_action == "trunction" { + + if request.manage_action == "truncation" { (processed_messages, token_counts) = agent .truncate_context(&request.messages) .await diff --git a/crates/goose/src/message.rs b/crates/goose/src/message.rs index 28c7aca168..5cbd621925 100644 --- a/crates/goose/src/message.rs +++ b/crates/goose/src/message.rs @@ -16,14 +16,17 @@ use mcp_core::role::Role; use mcp_core::tool::ToolCall; use serde::{Deserialize, Serialize}; use serde_json::Value; +use utoipa::ToSchema; mod tool_result_serde; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[derive(ToSchema)] pub struct ToolRequest { pub id: String, #[serde(with = "tool_result_serde")] + #[schema(value_type = Object)] pub tool_call: ToolResult, } @@ -45,14 +48,17 @@ impl ToolRequest { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[derive(ToSchema)] pub struct ToolResponse { pub id: String, #[serde(with = "tool_result_serde")] + #[schema(value_type = Object)] pub tool_result: ToolResult>, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[derive(ToSchema)] pub struct ToolConfirmationRequest { pub id: String, pub tool_name: String, @@ -60,31 +66,33 @@ pub struct ToolConfirmationRequest { pub prompt: Option, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] pub struct ThinkingContent { pub thinking: String, pub signature: String, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] pub struct RedactedThinkingContent { pub data: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[derive(ToSchema)] pub struct FrontendToolRequest { pub id: String, #[serde(with = "tool_result_serde")] + #[schema(value_type = Object)] pub tool_call: ToolResult, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] pub struct ContextLengthExceeded { pub msg: String, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] /// Content passed inside a message, which can be both simple content and tool content #[serde(tag = "type", rename_all = "camelCase")] pub enum MessageContent { @@ -273,7 +281,7 @@ impl From for Message { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)] /// A message to or from an LLM #[serde(rename_all = "camelCase")] pub struct Message { diff --git a/crates/mcp-core/src/content.rs b/crates/mcp-core/src/content.rs index 01c44be401..91ede3d80f 100644 --- a/crates/mcp-core/src/content.rs +++ b/crates/mcp-core/src/content.rs @@ -5,8 +5,9 @@ use super::role::Role; use crate::resource::ResourceContents; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Annotations { #[serde(skip_serializing_if = "Option::is_none")] @@ -14,6 +15,8 @@ pub struct Annotations { #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[schema(value_type = String, format = "date-time", example = "2023-01-01T00:00:00Z")] + // for openapi pub timestamp: Option>, } @@ -33,7 +36,7 @@ impl Annotations { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TextContent { pub text: String, @@ -41,7 +44,7 @@ pub struct TextContent { pub annotations: Option, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImageContent { pub data: String, @@ -50,7 +53,7 @@ pub struct ImageContent { pub annotations: Option, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EmbeddedResource { pub resource: ResourceContents, @@ -67,7 +70,7 @@ impl EmbeddedResource { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(ToSchema, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Content { Text(TextContent), diff --git a/crates/mcp-core/src/handler.rs b/crates/mcp-core/src/handler.rs index 338fe94e23..2a4c1a77ee 100644 --- a/crates/mcp-core/src/handler.rs +++ b/crates/mcp-core/src/handler.rs @@ -1,8 +1,11 @@ use async_trait::async_trait; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +#[allow(unused_imports)] // this is used in schema below +use serde_json::json; use serde_json::Value; use thiserror::Error; +use utoipa::ToSchema; #[non_exhaustive] #[derive(Error, Debug, Clone, Deserialize, Serialize, PartialEq)] @@ -19,6 +22,18 @@ pub enum ToolError { pub type ToolResult = std::result::Result; +// Define schema manually without generics issues +#[derive(ToSchema)] +#[schema(example = json!({"success": true, "data": {}}))] +pub struct ToolResultSchema { + #[schema(example = "Operation completed successfully")] + pub message: Option, + #[schema(example = true)] + pub success: bool, + #[schema(value_type = Object)] + pub data: Option, +} + #[derive(Error, Debug)] pub enum ResourceError { #[error("Execution failed: {0}")] diff --git a/crates/mcp-core/src/resource.rs b/crates/mcp-core/src/resource.rs index 8228f26156..73ad8064f4 100644 --- a/crates/mcp-core/src/resource.rs +++ b/crates/mcp-core/src/resource.rs @@ -1,10 +1,10 @@ +use crate::content::Annotations; /// Resources that servers provide to clients use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use url::Url; - -use crate::content::Annotations; +use utoipa::ToSchema; const EPSILON: f32 = 1e-6; // Tolerance for floating point comparison @@ -28,6 +28,7 @@ pub struct Resource { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(rename_all = "camelCase", untagged)] +#[derive(ToSchema)] pub enum ResourceContents { TextResourceContents { uri: String, diff --git a/crates/mcp-core/src/role.rs b/crates/mcp-core/src/role.rs index 38f3a87239..e15740d1a9 100644 --- a/crates/mcp-core/src/role.rs +++ b/crates/mcp-core/src/role.rs @@ -1,7 +1,8 @@ /// Roles to describe the origin/ownership of content use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum Role { User, diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 935cf96319..5ae8bb2403 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "1.0.20" + "version": "1.0.21" }, "paths": { "/agent/tools": { @@ -408,10 +408,76 @@ } } } + }, + "/context/manage": { + "post": { + "tags": [ + "Context Management" + ], + "operationId": "manage_context", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextManageRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Context managed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextManageResponse" + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing API key" + }, + "412": { + "description": "Precondition failed - Agent not available" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + } + ] + } } }, "components": { "schemas": { + "Annotations": { + "type": "object", + "properties": { + "audience": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Role" + }, + "nullable": true + }, + "priority": { + "type": "number", + "format": "float", + "nullable": true + }, + "timestamp": { + "type": "string", + "format": "date-time", + "example": "2023-01-01T00:00:00Z" + } + } + }, "ConfigKey": { "type": "object", "required": [ @@ -462,6 +528,152 @@ } } }, + "Content": { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "image" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/EmbeddedResource" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "resource" + ] + } + } + } + ] + } + ], + "discriminator": { + "propertyName": "type" + } + }, + "ContextLengthExceeded": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "type": "string" + } + } + }, + "ContextManageRequest": { + "type": "object", + "description": "Request payload for context management operations", + "required": [ + "messages", + "manageAction" + ], + "properties": { + "manageAction": { + "type": "string", + "description": "Operation to perform: \"truncation\" or \"summarize\"" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Collection of messages to be managed" + } + } + }, + "ContextManageResponse": { + "type": "object", + "description": "Response from context management operations", + "required": [ + "messages", + "tokenCounts" + ], + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Message" + }, + "description": "Processed messages after the operation" + }, + "tokenCounts": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "description": "Token counts for each processed message" + } + } + }, + "EmbeddedResource": { + "type": "object", + "required": [ + "resource" + ], + "properties": { + "annotations": { + "allOf": [ + { + "$ref": "#/components/schemas/Annotations" + } + ], + "nullable": true + }, + "resource": { + "$ref": "#/components/schemas/ResourceContents" + } + } + }, "Envs": { "type": "object", "additionalProperties": { @@ -704,6 +916,265 @@ } } }, + "FrontendToolRequest": { + "type": "object", + "required": [ + "id", + "toolCall" + ], + "properties": { + "id": { + "type": "string" + }, + "toolCall": { + "type": "object" + } + } + }, + "ImageContent": { + "type": "object", + "required": [ + "data", + "mimeType" + ], + "properties": { + "annotations": { + "allOf": [ + { + "$ref": "#/components/schemas/Annotations" + } + ], + "nullable": true + }, + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + } + } + }, + "Message": { + "type": "object", + "description": "A message to or from an LLM", + "required": [ + "role", + "created", + "content" + ], + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageContent" + } + }, + "created": { + "type": "integer", + "format": "int64" + }, + "role": { + "$ref": "#/components/schemas/Role" + } + } + }, + "MessageContent": { + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/TextContent" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "text" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/ImageContent" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "image" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/ToolRequest" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "toolRequest" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/ToolResponse" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "toolResponse" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/ToolConfirmationRequest" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "toolConfirmationRequest" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/FrontendToolRequest" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "frontendToolRequest" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/ThinkingContent" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "thinking" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/RedactedThinkingContent" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "redactedThinking" + ] + } + } + } + ] + }, + { + "allOf": [ + { + "$ref": "#/components/schemas/ContextLengthExceeded" + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "contextLengthExceeded" + ] + } + } + } + ] + } + ], + "description": "Content passed inside a message, which can be both simple content and tool content", + "discriminator": { + "propertyName": "type" + } + }, "ModelInfo": { "type": "object", "description": "Information about a model's capabilities", @@ -841,6 +1312,100 @@ } } }, + "RedactedThinkingContent": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "string" + } + } + }, + "ResourceContents": { + "oneOf": [ + { + "type": "object", + "required": [ + "uri", + "text" + ], + "properties": { + "mime_type": { + "type": "string", + "nullable": true + }, + "text": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "uri", + "blob" + ], + "properties": { + "blob": { + "type": "string" + }, + "mime_type": { + "type": "string", + "nullable": true + }, + "uri": { + "type": "string" + } + } + } + ] + }, + "Role": { + "type": "string", + "enum": [ + "user", + "assistant" + ] + }, + "TextContent": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "annotations": { + "allOf": [ + { + "$ref": "#/components/schemas/Annotations" + } + ], + "nullable": true + }, + "text": { + "type": "string" + } + } + }, + "ThinkingContent": { + "type": "object", + "required": [ + "thinking", + "signature" + ], + "properties": { + "signature": { + "type": "string" + }, + "thinking": { + "type": "string" + } + } + }, "Tool": { "type": "object", "description": "A tool that can be used by a model.", @@ -898,6 +1463,27 @@ } } }, + "ToolConfirmationRequest": { + "type": "object", + "required": [ + "id", + "toolName", + "arguments" + ], + "properties": { + "arguments": {}, + "id": { + "type": "string" + }, + "prompt": { + "type": "string", + "nullable": true + }, + "toolName": { + "type": "string" + } + } + }, "ToolInfo": { "type": "object", "description": "Information about the tool used for building prompts", @@ -945,6 +1531,61 @@ } } }, + "ToolRequest": { + "type": "object", + "required": [ + "id", + "toolCall" + ], + "properties": { + "id": { + "type": "string" + }, + "toolCall": { + "type": "object" + } + } + }, + "ToolResponse": { + "type": "object", + "required": [ + "id", + "toolResult" + ], + "properties": { + "id": { + "type": "string" + }, + "toolResult": { + "type": "object" + } + } + }, + "ToolResultSchema": { + "type": "object", + "required": [ + "success", + "data" + ], + "properties": { + "data": { + "type": "object" + }, + "message": { + "type": "string", + "example": "Operation completed successfully", + "nullable": true + }, + "success": { + "type": "boolean", + "example": true + } + }, + "example": { + "data": {}, + "success": true + } + }, "UpsertConfigQuery": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 048c09fc82..041710cc49 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, BackupConfigData, BackupConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ConfirmPermissionData } from './types.gen'; +import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, BackupConfigData, BackupConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ConfirmPermissionData, ManageContextData, ManageContextResponse } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -131,4 +131,15 @@ export const confirmPermission = (options: ...options?.headers } }); +}; + +export const manageContext = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + url: '/context/manage', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }); }; \ No newline at end of file diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 873d78cc68..d96ecbcbda 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -1,5 +1,11 @@ // This file is auto-generated by @hey-api/openapi-ts +export type Annotations = { + audience?: Array | null; + priority?: number | null; + timestamp?: string; +}; + export type ConfigKey = { default?: string | null; name: string; @@ -16,6 +22,51 @@ export type ConfigResponse = { config: {}; }; +export type Content = (TextContent & { + type: 'text'; +}) | (ImageContent & { + type: 'image'; +}) | (EmbeddedResource & { + type: 'resource'; +}); + +export type ContextLengthExceeded = { + msg: string; +}; + +/** + * Request payload for context management operations + */ +export type ContextManageRequest = { + /** + * Operation to perform: "truncation" or "summarize" + */ + manageAction: string; + /** + * Collection of messages to be managed + */ + messages: Array; +}; + +/** + * Response from context management operations + */ +export type ContextManageResponse = { + /** + * Processed messages after the operation + */ + messages: Array; + /** + * Token counts for each processed message + */ + tokenCounts: Array; +}; + +export type EmbeddedResource = { + annotations?: Annotations | null; + resource: ResourceContents; +}; + export type Envs = { [key: string]: string; }; @@ -102,6 +153,51 @@ export type ExtensionResponse = { extensions: Array; }; +export type FrontendToolRequest = { + id: string; + toolCall: { + [key: string]: unknown; + }; +}; + +export type ImageContent = { + annotations?: Annotations | null; + data: string; + mimeType: string; +}; + +/** + * A message to or from an LLM + */ +export type Message = { + content: Array; + created: number; + role: Role; +}; + +/** + * Content passed inside a message, which can be both simple content and tool content + */ +export type MessageContent = (TextContent & { + type: 'text'; +}) | (ImageContent & { + type: 'image'; +}) | (ToolRequest & { + type: 'toolRequest'; +}) | (ToolResponse & { + type: 'toolResponse'; +}) | (ToolConfirmationRequest & { + type: 'toolConfirmationRequest'; +}) | (FrontendToolRequest & { + type: 'frontendToolRequest'; +}) | (ThinkingContent & { + type: 'thinking'; +}) | (RedactedThinkingContent & { + type: 'redactedThinking'; +}) | (ContextLengthExceeded & { + type: 'contextLengthExceeded'; +}); + /** * Information about a model's capabilities */ @@ -180,6 +276,32 @@ export type ProvidersResponse = { providers: Array; }; +export type RedactedThinkingContent = { + data: string; +}; + +export type ResourceContents = { + mime_type?: string | null; + text: string; + uri: string; +} | { + blob: string; + mime_type?: string | null; + uri: string; +}; + +export type Role = 'user' | 'assistant'; + +export type TextContent = { + annotations?: Annotations | null; + text: string; +}; + +export type ThinkingContent = { + signature: string; + thinking: string; +}; + /** * A tool that can be used by a model. */ @@ -249,6 +371,13 @@ export type ToolAnnotations = { title?: string | null; }; +export type ToolConfirmationRequest = { + arguments: unknown; + id: string; + prompt?: string | null; + toolName: string; +}; + /** * Information about the tool used for building prompts */ @@ -267,6 +396,28 @@ export type ToolPermission = { tool_name: string; }; +export type ToolRequest = { + id: string; + toolCall: { + [key: string]: unknown; + }; +}; + +export type ToolResponse = { + id: string; + toolResult: { + [key: string]: unknown; + }; +}; + +export type ToolResultSchema = { + data: { + [key: string]: unknown; + }; + message?: string | null; + success: boolean; +}; + export type UpsertConfigQuery = { is_secret: boolean; key: string; @@ -593,6 +744,37 @@ export type ConfirmPermissionResponses = { 200: unknown; }; +export type ManageContextData = { + body: ContextManageRequest; + path?: never; + query?: never; + url: '/context/manage'; +}; + +export type ManageContextErrors = { + /** + * Unauthorized - Invalid or missing API key + */ + 401: unknown; + /** + * Precondition failed - Agent not available + */ + 412: unknown; + /** + * Internal server error + */ + 500: unknown; +}; + +export type ManageContextResponses = { + /** + * Context managed successfully + */ + 200: ContextManageResponse; +}; + +export type ManageContextResponse = ManageContextResponses[keyof ManageContextResponses]; + export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}); }; \ No newline at end of file diff --git a/ui/desktop/src/components/ChatView.tsx b/ui/desktop/src/components/ChatView.tsx index 25f597f465..4b29878d0f 100644 --- a/ui/desktop/src/components/ChatView.tsx +++ b/ui/desktop/src/components/ChatView.tsx @@ -16,11 +16,12 @@ import { createRecipe } from '../recipe'; import { AgentHeader } from './AgentHeader'; import LayingEggLoader from './LayingEggLoader'; import { fetchSessionDetails } from '../sessions'; -// import { configureRecipeExtensions } from '../utils/recipeExtensions'; import 'react-toastify/dist/ReactToastify.css'; import { useMessageStream } from '../hooks/useMessageStream'; import { SessionSummaryModal } from './context_management/SessionSummaryModal'; import { Recipe } from '../recipe'; +import { ContextManagerProvider, useChatContextManager } from './context_management/ContextManager'; +import { ContextLengthExceededHandler } from './context_management/ContextLengthExceededHandler'; import { Message, createUserMessage, @@ -30,7 +31,6 @@ import { ToolResponseMessageContent, ToolConfirmationRequestMessageContent, } from '../types/message'; -import { manageContext } from './context_management'; export interface ChatType { id: string; @@ -63,6 +63,30 @@ export default function ChatView({ setView: (view: View, viewOptions?: ViewOptions) => void; setIsGoosehintsModalOpen: (isOpen: boolean) => void; }) { + + return ( + + + + ); +} + +function ChatContent({ + chat, + setChat, + setView, + setIsGoosehintsModalOpen, +}: { + chat: ChatType; + setChat: (chat: ChatType) => void; + setView: (view: View, viewOptions?: ViewOptions) => void; + setIsGoosehintsModalOpen: (isOpen: boolean) => void; +}) { // Disabled askAi calls to save costs // const [messageMetadata, setMessageMetadata] = useState>({}); const [hasMessages, setHasMessages] = useState(false); @@ -72,15 +96,24 @@ export default function ChatView({ const [sessionTokenCount, setSessionTokenCount] = useState(0); const scrollRef = useRef(null); - const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); - const [summaryContent, setSummaryContent] = useState(''); - const [summarizedThread, setSummarizedThread] = useState([]); + const { + summaryContent, + summarizedThread, + isSummaryModalOpen, + resetMessagesWithSummary, + closeSummaryModal, + updateSummary, + hasContextLengthExceededContent, + } = useChatContextManager(); - // Add this function to handle opening the summary modal with content - const handleViewSummary = (summary: string) => { - setSummaryContent(summary); - setIsSummaryModalOpen(true); - }; + useEffect(() => { + // Log all messages when the component first mounts + console.log('Initial messages when resuming session:', chat.messages); + window.electron.logInfo( + 'Initial messages when resuming session: ' + JSON.stringify(chat.messages, null, 2) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Empty dependency array means this runs once on mount; // Get recipeConfig directly from appConfig const recipeConfig = window.appConfig.get('recipeConfig') as Recipe | null; @@ -103,6 +136,12 @@ export default function ChatView({ onFinish: async (_message, _reason) => { window.electron.stopPowerSaveBlocker(); + setTimeout(() => { + if (scrollRef.current?.scrollToBottom) { + scrollRef.current.scrollToBottom(); + } + }, 300); + // Disabled askAi calls to save costs // const messageText = getTextContent(message); // const fetchResponses = await askAi(messageText); @@ -118,11 +157,6 @@ export default function ChatView({ }); } }, - onToolCall: (toolCall: string) => { - // Handle tool calls if needed - console.log('Tool call received:', toolCall); - // Implement tool call handling logic here - }, }); // Listen for make-agent-from-chat event @@ -201,11 +235,27 @@ export default function ChatView({ window.electron.startPowerSaveBlocker(); const customEvent = e as CustomEvent; const content = customEvent.detail?.value || ''; + if (content.trim()) { setLastInteractionTime(Date.now()); - append(createUserMessage(content)); - if (scrollRef.current?.scrollToBottom) { - scrollRef.current.scrollToBottom(); + + if (process.env.ALPHA && summarizedThread.length > 0) { + // First reset the messages with the summary + resetMessagesWithSummary(messages, setMessages); + + // Then append the new user message + setTimeout(() => { + append(createUserMessage(content)); + if (scrollRef.current?.scrollToBottom) { + scrollRef.current.scrollToBottom(); + } + }, 150); // Small delay to ensure state updates properly + } else { + // Normal flow - just append the message + append(createUserMessage(content)); + if (scrollRef.current?.scrollToBottom) { + scrollRef.current.scrollToBottom(); + } } } }; @@ -300,55 +350,12 @@ export default function ChatView({ } }; - // Add this function to ChatView.tsx to detect if a message contains ContextLengthExceededContent - const hasContextLengthExceededContent = (message: Message): boolean => { - return message.content.some((content) => content.type === 'contextLengthExceeded'); - }; - - const handleContextLengthExceeded = async () => { - // If we already have a summary, use that - if (summaryContent) { - return summaryContent; - } - - // Otherwise, generate a summary - const response = await manageContext({ messages: messages, manageAction: 'summarize' }); - setSummarizedThread(response.messages); - return response.messages[0].text; - }; - - const SummarizedNotification = ({ - onViewSummary, - }: { - onViewSummary: (summaryContent: string) => void; - }) => { - const handleViewSummary = async () => { - // Await the result to get a string - const summary = summaryContent || (await handleContextLengthExceeded()); - onViewSummary(summary); // Now always passing a string - }; - - return ( -
- Session summarized - -
- ); - }; - // Filter out standalone tool response messages for rendering // They will be shown as part of the tool invocation in the assistant message const filteredMessages = messages.filter((message) => { - // TODO: use this summarized thread in the chat window - if (summarizedThread.length > 0) { - // we have a summarized thread - console.log('summarized thread has been created --', summarizedThread); - } + // Only filter out when display is explicitly false + if (message.display === false) return false; + // Keep all assistant messages and user messages that aren't just tool responses if (message.role === 'assistant') return true; @@ -440,8 +447,10 @@ export default function ChatView({ <> {/* Only render GooseMessage if it's not a CLE message (and we are not in alpha mode) */} {process.env.ALPHA && hasContextLengthExceededContent(message) ? ( - // Render the summarized notification for CLE messages only in alpha mode - + ) : ( setIsSummaryModalOpen(false)} + onClose={closeSummaryModal} onSave={(editedContent) => { - console.log('Saving summary...'); - setSummaryContent(editedContent); - setIsSummaryModalOpen(false); + updateSummary(editedContent); + closeSummaryModal(); }} summaryContent={summaryContent} /> diff --git a/ui/desktop/src/components/context_management/ContextLengthExceededHandler.tsx b/ui/desktop/src/components/context_management/ContextLengthExceededHandler.tsx new file mode 100644 index 0000000000..06662e5e50 --- /dev/null +++ b/ui/desktop/src/components/context_management/ContextLengthExceededHandler.tsx @@ -0,0 +1,77 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Message } from '../../types/message'; +import { useChatContextManager } from './ContextManager'; + +interface ContextLengthExceededHandlerProps { + messages: Message[]; + messageId: string; +} + +export const ContextLengthExceededHandler: React.FC = ({ + messages, + messageId, +}) => { + const { fetchSummary, summaryContent, isLoadingSummary, errorLoadingSummary, openSummaryModal } = + useChatContextManager(); + + const [hasFetchStarted, setHasFetchStarted] = useState(false); + + // Find the relevant message to check if it's the latest + const isCurrentMessageLatest = + messageId === messages[messages.length - 1].id || + messageId === messages[messages.length - 1].created.toString(); + + // Only allow interaction for the most recent context length exceeded event + const shouldAllowSummaryInteraction = isCurrentMessageLatest; + + // Use a ref to track if we've started the fetch + const fetchStartedRef = useRef(false); + + useEffect(() => { + // Automatically fetch summary if conditions are met + if ( + !summaryContent && + !hasFetchStarted && + shouldAllowSummaryInteraction && + !fetchStartedRef.current + ) { + setHasFetchStarted(true); + fetchStartedRef.current = true; + fetchSummary(messages); + } + }, [fetchSummary, hasFetchStarted, messages, shouldAllowSummaryInteraction, summaryContent]); + + // Handle retry + const handleRetry = () => { + if (!shouldAllowSummaryInteraction) return; + fetchSummary(messages); + }; + + // Render the notification UI + return ( +
+ {isLoadingSummary && shouldAllowSummaryInteraction ? ( + // Only show loading indicator during loading state +
+ Preparing summary... + +
+ ) : ( + // Show different UI based on whether it's already handled + <> + {'Session summarized'} + + {/* Only show the button if its last message */} + {shouldAllowSummaryInteraction && ( + + )} + + )} +
+ ); +}; diff --git a/ui/desktop/src/components/context_management/ContextManager.tsx b/ui/desktop/src/components/context_management/ContextManager.tsx new file mode 100644 index 0000000000..55c01629c2 --- /dev/null +++ b/ui/desktop/src/components/context_management/ContextManager.tsx @@ -0,0 +1,170 @@ +import React, { createContext, useContext, useState } from 'react'; +import { Message } from '../../types/message'; +import { manageContextFromBackend, convertApiMessageToFrontendMessage } from './index'; + +// Define the context management interface +interface ContextManagerState { + summaryContent: string; + summarizedThread: Message[]; + isSummaryModalOpen: boolean; + isLoadingSummary: boolean; + errorLoadingSummary: boolean; +} + +interface ContextManagerActions { + fetchSummary: (messages: Message[]) => Promise; + updateSummary: (newSummaryContent: string) => void; + resetMessagesWithSummary: ( + messages: Message[], + setMessages: (messages: Message[]) => void + ) => void; + openSummaryModal: () => void; + closeSummaryModal: () => void; + hasContextLengthExceededContent: (message: Message) => boolean; +} + +// Create the context +const ContextManagerContext = createContext< + (ContextManagerState & ContextManagerActions) | undefined +>(undefined); + +// Create the provider component +export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [summaryContent, setSummaryContent] = useState(''); + const [summarizedThread, setSummarizedThread] = useState([]); + const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); + const [isLoadingSummary, setIsLoadingSummary] = useState(false); + const [errorLoadingSummary, setErrorLoadingSummary] = useState(false); + + const fetchSummary = async (messages: Message[]) => { + setIsLoadingSummary(true); + setErrorLoadingSummary(false); + + try { + const response = await manageContextFromBackend({ + messages: messages, + manageAction: 'summarize', + }); + + // Convert API messages to frontend messages + const convertedMessages = response.messages.map((apiMessage) => + convertApiMessageToFrontendMessage(apiMessage) + ); + + // Extract the summary text from the first message + const summaryMessage = convertedMessages[0].content[0]; + if (summaryMessage.type === 'text') { + const summary = summaryMessage.text; + setSummaryContent(summary); + setSummarizedThread(convertedMessages); + } + + setIsLoadingSummary(false); + } catch (err) { + console.error('Error fetching summary:', err); + setErrorLoadingSummary(true); + setIsLoadingSummary(false); + } + }; + + const updateSummary = (newSummaryContent: string) => { + // Update the summary content + setSummaryContent(newSummaryContent); + + // Update the thread if it exists + if (summarizedThread.length > 0) { + // Create a deep copy of the thread + const updatedThread = [...summarizedThread]; + + // Create a copy of the first message + const firstMessage = { ...updatedThread[0] }; + + // Create a copy of the content array + const updatedContent = [...firstMessage.content]; + + // Update the summary text in the first content item + if (updatedContent[0] && updatedContent[0].type === 'text') { + updatedContent[0] = { + ...updatedContent[0], + text: newSummaryContent, + }; + } + + // Update the message with the new content + firstMessage.content = updatedContent; + updatedThread[0] = firstMessage; + + // Update the thread + setSummarizedThread(updatedThread); + } + }; + + const resetMessagesWithSummary = ( + messages: Message[], + setMessages: (messages: Message[]) => void + ) => { + // Update summarizedThread with metadata + const updatedSummarizedThread = summarizedThread.map((msg) => ({ + ...msg, + display: false, + sendToLLM: true, + })); + + // Update list of messages with other metadata + const updatedMessages = messages.map((msg) => ({ + ...msg, + display: true, + sendToLLM: false, + })); + + // Make a copy that combines both + const newMessages = [...updatedMessages, ...updatedSummarizedThread]; + + // Update the messages state + setMessages(newMessages); + + // Clear the summarized thread and content + setSummarizedThread([]); + setSummaryContent(''); + }; + + const hasContextLengthExceededContent = (message: Message): boolean => { + return message.content.some((content) => content.type === 'contextLengthExceeded'); + }; + + const openSummaryModal = () => { + setIsSummaryModalOpen(true); + }; + + const closeSummaryModal = () => { + setIsSummaryModalOpen(false); + }; + + const value = { + // State + summaryContent, + summarizedThread, + isSummaryModalOpen, + isLoadingSummary, + errorLoadingSummary, + + // Actions + fetchSummary, + updateSummary, + resetMessagesWithSummary, + openSummaryModal, + closeSummaryModal, + hasContextLengthExceededContent, + }; + + return {children}; +}; + +// Create a hook to use the context +export const useChatContextManager = () => { + const context = useContext(ContextManagerContext); + if (context === undefined) { + throw new Error('useContextManager must be used within a ContextManagerProvider'); + } + return context; +}; diff --git a/ui/desktop/src/components/context_management/index.ts b/ui/desktop/src/components/context_management/index.ts index 137e521b59..52c1298d05 100644 --- a/ui/desktop/src/components/context_management/index.ts +++ b/ui/desktop/src/components/context_management/index.ts @@ -1,37 +1,119 @@ -import { Message } from '../../types/message'; -import { getApiUrl, getSecretKey } from '../../config'; +import { + Message as FrontendMessage, + Content as FrontendContent, + MessageContent as FrontendMessageContent, + ToolCallResult, + ToolCall, +} from '../../types/message'; +import { + ContextManageRequest, + ContextManageResponse, + manageContext, + Message as ApiMessage, + MessageContent as ApiMessageContent, +} from '../../api'; +import { generateId } from 'ai'; -export async function manageContext({ +export async function manageContextFromBackend({ messages, manageAction, }: { - messages: Message[]; - manageAction: 'trunction' | 'summarize'; -}) { - const response = await fetch(getApiUrl('/context/manage'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - messages, - manageAction, - }), - }); - if (!response.ok) { - if (!response.ok) { - // Get the status text or a default message - const errorText = await response.text().catch(() => 'Unknown error'); - // log error with status and details - console.error( - `Context management failed: ${response.status} ${response.statusText} - ${errorText}` - ); - throw new Error( - `Context management failed: ${response.status} ${response.statusText} - ${errorText}\n\nStart a new session.` - ); + messages: FrontendMessage[]; + manageAction: 'truncation' | 'summarize'; +}): Promise { + try { + const contextManagementRequest = { manageAction, messages }; + + // Cast to the API-expected type + const result = await manageContext({ + body: contextManagementRequest as unknown as ContextManageRequest, + }); + + // Check for errors in the result + if (result.error) { + throw new Error(`Context management failed: ${result.error}`); } + + // Extract the actual data from the result + if (!result.data) { + throw new Error('Context management returned no data'); + } + + return result.data; + } catch (error) { + console.error(`Context management failed: ${error || 'Unknown error'}`); + throw new Error( + `Context management failed: ${error || 'Unknown error'}\n\nStart a new session.` + ); } - const data = await response.json(); - return data; +} + +// Function to convert API Message to frontend Message +export function convertApiMessageToFrontendMessage(apiMessage: ApiMessage): FrontendMessage { + return { + display: false, + sendToLLM: false, + id: generateId(), + role: apiMessage.role, + created: apiMessage.created, + content: apiMessage.content + .map((apiContent) => mapApiContentToFrontendMessageContent(apiContent)) + .filter((content): content is FrontendMessageContent => content !== null), + }; +} + +// Function to convert API MessageContent to frontend MessageContent +function mapApiContentToFrontendMessageContent( + apiContent: ApiMessageContent +): FrontendMessageContent | null { + // Handle each content type specifically based on its "type" property + if (apiContent.type === 'text') { + return { + type: 'text', + text: apiContent.text, + annotations: apiContent.annotations as Record | undefined, + }; + } else if (apiContent.type === 'image') { + return { + type: 'image', + data: apiContent.data, + mimeType: apiContent.mimeType, + annotations: apiContent.annotations as Record | undefined, + }; + } else if (apiContent.type === 'toolRequest') { + // Ensure the toolCall has the correct type structure + const toolCall = apiContent.toolCall as unknown as ToolCallResult; + + return { + type: 'toolRequest', + id: apiContent.id, + toolCall: toolCall, + }; + } else if (apiContent.type === 'toolResponse') { + // Ensure the toolResult has the correct type structure + const toolResult = apiContent.toolResult as unknown as ToolCallResult; + + return { + type: 'toolResponse', + id: apiContent.id, + toolResult: toolResult, + }; + } else if (apiContent.type === 'toolConfirmationRequest') { + return { + type: 'toolConfirmationRequest', + id: apiContent.id, + toolName: apiContent.toolName, + arguments: apiContent.arguments as Record, + prompt: apiContent.prompt === null ? undefined : apiContent.prompt, + }; + } else if (apiContent.type === 'contextLengthExceeded') { + return { + type: 'contextLengthExceeded', + msg: apiContent.msg, + }; + } + + // For types that exist in API but not in frontend, either skip or convert + console.warn(`Skipping unsupported content type: ${apiContent.type}`); + return null; } diff --git a/ui/desktop/src/components/context_management/sessionManagement.ts b/ui/desktop/src/components/context_management/sessionManagement.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui/desktop/src/hooks/useMessageStream.ts b/ui/desktop/src/hooks/useMessageStream.ts index 6b4ee3eb72..9ad5654433 100644 --- a/ui/desktop/src/hooks/useMessageStream.ts +++ b/ui/desktop/src/hooks/useMessageStream.ts @@ -215,11 +215,26 @@ export function useMessageStream({ const parsedEvent = JSON.parse(data) as MessageEvent; switch (parsedEvent.type) { - case 'Message': + case 'Message': { + // Create a new message object with the properties preserved or defaulted + const newMessage = { + ...parsedEvent.message, + // Only set to true if it's undefined (preserve false values) + display: + parsedEvent.message.display === undefined + ? true + : parsedEvent.message.display, + sendToLLM: + parsedEvent.message.sendToLLM === undefined + ? true + : parsedEvent.message.sendToLLM, + }; + // Update messages with the new message - currentMessages = [...currentMessages, parsedEvent.message]; + currentMessages = [...currentMessages, newMessage]; mutate(currentMessages, false); break; + } case 'Error': throw new Error(parsedEvent.error); @@ -268,9 +283,12 @@ export function useMessageStream({ const abortController = new AbortController(); abortControllerRef.current = abortController; + // Filter out messages where sendToLLM is explicitly false + const filteredMessages = requestMessages.filter((message) => message.sendToLLM !== false); + // Log request details for debugging console.log('Request details:', { - messages: requestMessages, + messages: filteredMessages, body: extraMetadataRef.current.body, }); diff --git a/ui/desktop/src/types/message.ts b/ui/desktop/src/types/message.ts index 7c0607ffa8..f6227327e3 100644 --- a/ui/desktop/src/types/message.ts +++ b/ui/desktop/src/types/message.ts @@ -91,6 +91,8 @@ export interface Message { role: Role; created: number; content: MessageContent[]; + display: boolean; + sendToLLM: boolean; } // Helper functions to create messages