mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 22:54:24 +01:00
context_management: handle summarization in UI (#2377)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<Message>,
|
||||
manage_action: String,
|
||||
/// Collection of messages to be managed
|
||||
pub messages: Vec<Message>,
|
||||
/// 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<Message>,
|
||||
token_counts: Vec<usize>,
|
||||
/// Processed messages after the operation
|
||||
pub messages: Vec<Message>,
|
||||
/// Token counts for each processed message
|
||||
pub token_counts: Vec<usize>,
|
||||
}
|
||||
|
||||
#[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<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
@@ -40,7 +60,8 @@ async fn manage_context(
|
||||
|
||||
let mut processed_messages: Vec<Message> = vec![];
|
||||
let mut token_counts: Vec<usize> = vec![];
|
||||
if request.manage_action == "trunction" {
|
||||
|
||||
if request.manage_action == "truncation" {
|
||||
(processed_messages, token_counts) = agent
|
||||
.truncate_context(&request.messages)
|
||||
.await
|
||||
|
||||
@@ -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<ToolCall>,
|
||||
}
|
||||
|
||||
@@ -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<Vec<Content>>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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<ToolCall>,
|
||||
}
|
||||
|
||||
#[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<PromptMessage> 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 {
|
||||
|
||||
@@ -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<f32>,
|
||||
#[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<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
@@ -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<Annotations>,
|
||||
}
|
||||
|
||||
#[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<Annotations>,
|
||||
}
|
||||
|
||||
#[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),
|
||||
|
||||
@@ -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<T> = std::result::Result<T, ToolError>;
|
||||
|
||||
// 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<String>,
|
||||
#[schema(example = true)]
|
||||
pub success: bool,
|
||||
#[schema(value_type = Object)]
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ResourceError {
|
||||
#[error("Execution failed: {0}")]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||
@@ -132,3 +132,14 @@ export const confirmPermission = <ThrowOnError extends boolean = false>(options:
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const manageContext = <ThrowOnError extends boolean = false>(options: Options<ManageContextData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<ManageContextResponse, unknown, ThrowOnError>({
|
||||
url: '/context/manage',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,11 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type Annotations = {
|
||||
audience?: Array<Role> | 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<Message>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Response from context management operations
|
||||
*/
|
||||
export type ContextManageResponse = {
|
||||
/**
|
||||
* Processed messages after the operation
|
||||
*/
|
||||
messages: Array<Message>;
|
||||
/**
|
||||
* Token counts for each processed message
|
||||
*/
|
||||
tokenCounts: Array<number>;
|
||||
};
|
||||
|
||||
export type EmbeddedResource = {
|
||||
annotations?: Annotations | null;
|
||||
resource: ResourceContents;
|
||||
};
|
||||
|
||||
export type Envs = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -102,6 +153,51 @@ export type ExtensionResponse = {
|
||||
extensions: Array<ExtensionEntry>;
|
||||
};
|
||||
|
||||
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<MessageContent>;
|
||||
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<ProviderDetails>;
|
||||
};
|
||||
|
||||
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 & {});
|
||||
};
|
||||
@@ -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 (
|
||||
<ContextManagerProvider>
|
||||
<ChatContent
|
||||
chat={chat}
|
||||
setChat={setChat}
|
||||
setView={setView}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
/>
|
||||
</ContextManagerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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<Record<string, string[]>>({});
|
||||
const [hasMessages, setHasMessages] = useState(false);
|
||||
@@ -72,15 +96,24 @@ export default function ChatView({
|
||||
const [sessionTokenCount, setSessionTokenCount] = useState<number>(0);
|
||||
const scrollRef = useRef<ScrollAreaHandle>(null);
|
||||
|
||||
const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false);
|
||||
const [summaryContent, setSummaryContent] = useState('');
|
||||
const [summarizedThread, setSummarizedThread] = useState<Message[]>([]);
|
||||
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,12 +235,28 @@ export default function ChatView({
|
||||
window.electron.startPowerSaveBlocker();
|
||||
const customEvent = e as CustomEvent;
|
||||
const content = customEvent.detail?.value || '';
|
||||
|
||||
if (content.trim()) {
|
||||
setLastInteractionTime(Date.now());
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col items-start mt-1 pl-4">
|
||||
<span className="text-xs text-gray-400 italic">Session summarized</span>
|
||||
<button
|
||||
onClick={handleViewSummary}
|
||||
className="text-xs text-textStandard cursor-pointer hover:text-textSubtle transition-colors mt-1"
|
||||
>
|
||||
View or edit summary
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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
|
||||
<SummarizedNotification onViewSummary={handleViewSummary} />
|
||||
<ContextLengthExceededHandler
|
||||
messages={messages}
|
||||
messageId={message.id ?? message.created.toString()}
|
||||
/>
|
||||
) : (
|
||||
<GooseMessage
|
||||
messageHistoryIndex={chat?.messageHistoryIndex}
|
||||
@@ -502,11 +511,10 @@ export default function ChatView({
|
||||
{process.env.ALPHA && (
|
||||
<SessionSummaryModal
|
||||
isOpen={isSummaryModalOpen}
|
||||
onClose={() => setIsSummaryModalOpen(false)}
|
||||
onClose={closeSummaryModal}
|
||||
onSave={(editedContent) => {
|
||||
console.log('Saving summary...');
|
||||
setSummaryContent(editedContent);
|
||||
setIsSummaryModalOpen(false);
|
||||
updateSummary(editedContent);
|
||||
closeSummaryModal();
|
||||
}}
|
||||
summaryContent={summaryContent}
|
||||
/>
|
||||
|
||||
@@ -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<ContextLengthExceededHandlerProps> = ({
|
||||
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 (
|
||||
<div className="flex flex-col items-start mt-1 pl-4">
|
||||
{isLoadingSummary && shouldAllowSummaryInteraction ? (
|
||||
// Only show loading indicator during loading state
|
||||
<div className="flex items-center text-xs text-gray-400">
|
||||
<span className="mr-2">Preparing summary...</span>
|
||||
<span className="animate-spin h-3 w-3 border-2 border-gray-400 rounded-full border-t-transparent"></span>
|
||||
</div>
|
||||
) : (
|
||||
// Show different UI based on whether it's already handled
|
||||
<>
|
||||
<span className="text-xs text-gray-400 italic">{'Session summarized'}</span>
|
||||
|
||||
{/* Only show the button if its last message */}
|
||||
{shouldAllowSummaryInteraction && (
|
||||
<button
|
||||
onClick={() => (errorLoadingSummary ? handleRetry() : openSummaryModal())}
|
||||
className="text-xs text-textStandard hover:text-textSubtle transition-colors mt-1 flex items-center"
|
||||
>
|
||||
{errorLoadingSummary ? 'Retry loading summary' : 'View or edit summary'}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
170
ui/desktop/src/components/context_management/ContextManager.tsx
Normal file
170
ui/desktop/src/components/context_management/ContextManager.tsx
Normal file
@@ -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<void>;
|
||||
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<string>('');
|
||||
const [summarizedThread, setSummarizedThread] = useState<Message[]>([]);
|
||||
const [isSummaryModalOpen, setIsSummaryModalOpen] = useState<boolean>(false);
|
||||
const [isLoadingSummary, setIsLoadingSummary] = useState<boolean>(false);
|
||||
const [errorLoadingSummary, setErrorLoadingSummary] = useState<boolean>(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 <ContextManagerContext.Provider value={value}>{children}</ContextManagerContext.Provider>;
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
@@ -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,
|
||||
}),
|
||||
messages: FrontendMessage[];
|
||||
manageAction: 'truncation' | 'summarize';
|
||||
}): Promise<ContextManageResponse> {
|
||||
try {
|
||||
const contextManagementRequest = { manageAction, messages };
|
||||
|
||||
// Cast to the API-expected type
|
||||
const result = await manageContext({
|
||||
body: contextManagementRequest as unknown as ContextManageRequest,
|
||||
});
|
||||
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}`
|
||||
);
|
||||
|
||||
// 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: ${response.status} ${response.statusText} - ${errorText}\n\nStart a new session.`
|
||||
`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<string, unknown> | undefined,
|
||||
};
|
||||
} else if (apiContent.type === 'image') {
|
||||
return {
|
||||
type: 'image',
|
||||
data: apiContent.data,
|
||||
mimeType: apiContent.mimeType,
|
||||
annotations: apiContent.annotations as Record<string, unknown> | undefined,
|
||||
};
|
||||
} else if (apiContent.type === 'toolRequest') {
|
||||
// Ensure the toolCall has the correct type structure
|
||||
const toolCall = apiContent.toolCall as unknown as ToolCallResult<ToolCall>;
|
||||
|
||||
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<FrontendContent[]>;
|
||||
|
||||
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<string, unknown>,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ export interface Message {
|
||||
role: Role;
|
||||
created: number;
|
||||
content: MessageContent[];
|
||||
display: boolean;
|
||||
sendToLLM: boolean;
|
||||
}
|
||||
|
||||
// Helper functions to create messages
|
||||
|
||||
Reference in New Issue
Block a user