mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
First pass at Title Summarizer
This commit is contained in:
parent
13534cc8b7
commit
415e64fcd9
5 changed files with 420 additions and 0 deletions
|
|
@ -146,6 +146,8 @@ export const globalSettingsSchema = z.object({
|
|||
customSupportPrompts: customSupportPromptsSchema.optional(),
|
||||
enhancementApiConfigId: z.string().optional(),
|
||||
includeTaskHistoryInEnhance: z.boolean().optional(),
|
||||
autoSummarizeLongTitles: z.boolean().optional(),
|
||||
titleSummarizationThreshold: z.number().min(50).optional(),
|
||||
historyPreviewCollapsed: z.boolean().optional(),
|
||||
reasoningBlockCollapsed: z.boolean().optional(),
|
||||
profileThresholds: z.record(z.string(), z.number()).optional(),
|
||||
|
|
@ -318,6 +320,9 @@ export const EVALS_SETTINGS: RooCodeSettings = {
|
|||
|
||||
mode: "code", // "architect",
|
||||
|
||||
autoSummarizeLongTitles: true,
|
||||
titleSummarizationThreshold: 150,
|
||||
|
||||
customModes: [],
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ import type { ClineMessage } from "@roo-code/types"
|
|||
import { readApiMessages, saveApiMessages, saveTaskMessages } from "../task-persistence"
|
||||
import { getNonce } from "./getNonce"
|
||||
import { getUri } from "./getUri"
|
||||
import { TitleSummarizer } from "./titleSummarizer"
|
||||
|
||||
/**
|
||||
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
|
|
@ -2570,9 +2571,71 @@ export class ClineProvider
|
|||
`[createTask] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`,
|
||||
)
|
||||
|
||||
// Trigger asynchronous title summarization for long task messages
|
||||
const autoSummarize = this.contextProxy.getValue("autoSummarizeLongTitles") ?? true
|
||||
const threshold = this.contextProxy.getValue("titleSummarizationThreshold") ?? 150
|
||||
|
||||
if (autoSummarize && text && text.length > threshold) {
|
||||
this.summarizeTaskTitle(task.taskId, text).catch((error: unknown) => {
|
||||
this.log(`Failed to summarize task title: ${error instanceof Error ? error.message : String(error)}`)
|
||||
})
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize long task titles asynchronously and update the task history
|
||||
* @param taskId - The ID of the task to update
|
||||
* @param originalText - The original task text to summarize
|
||||
*/
|
||||
private async summarizeTaskTitle(taskId: string, originalText: string): Promise<void> {
|
||||
try {
|
||||
// Get the current state to check for API configuration
|
||||
const state = await this.getState()
|
||||
|
||||
// Get the configurable threshold
|
||||
const threshold = this.contextProxy.getValue("titleSummarizationThreshold") ?? 150
|
||||
|
||||
// Try to summarize the title
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: originalText,
|
||||
apiConfiguration: state.apiConfiguration,
|
||||
customSupportPrompts: state.customSupportPrompts,
|
||||
enhancementApiConfigId: state.enhancementApiConfigId,
|
||||
listApiConfigMeta: state.listApiConfigMeta,
|
||||
providerSettingsManager: this.providerSettingsManager,
|
||||
maxLength: threshold,
|
||||
})
|
||||
|
||||
// If summarization succeeded and produced a shorter title, update the task history
|
||||
if (result.success && result.summarizedTitle && result.summarizedTitle.length < originalText.length) {
|
||||
const history = this.getGlobalState("taskHistory") ?? []
|
||||
const taskHistoryItem = history.find((item) => item.id === taskId)
|
||||
|
||||
if (taskHistoryItem) {
|
||||
// Update the task field with the summarized version
|
||||
taskHistoryItem.task = result.summarizedTitle
|
||||
|
||||
// Update the history
|
||||
await this.updateTaskHistory(taskHistoryItem)
|
||||
|
||||
// Capture telemetry
|
||||
TitleSummarizer.captureTelemetry(taskId, originalText.length, result.summarizedTitle.length)
|
||||
|
||||
this.log(
|
||||
`Task title summarized from ${originalText.length} to ${result.summarizedTitle.length} characters`,
|
||||
)
|
||||
}
|
||||
} else if (!result.success && result.error) {
|
||||
this.log(`Title summarization failed: ${result.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - title summarization is not critical
|
||||
this.log(`Title summarization error: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
public async cancelTask(): Promise<void> {
|
||||
const task = this.getCurrentTask()
|
||||
|
||||
|
|
|
|||
213
src/core/webview/__tests__/titleSummarizer.spec.ts
Normal file
213
src/core/webview/__tests__/titleSummarizer.spec.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import { TitleSummarizer } from "../titleSummarizer"
|
||||
import type { ProviderSettings, ProviderSettingsEntry } from "@roo-code/types"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
import { singleCompletionHandler } from "../../../utils/single-completion-handler"
|
||||
|
||||
// Mock TelemetryService
|
||||
vi.mock("@roo-code/telemetry", () => ({
|
||||
TelemetryService: {
|
||||
instance: {
|
||||
captureEvent: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock singleCompletionHandler
|
||||
vi.mock("../../../utils/single-completion-handler", () => ({
|
||||
singleCompletionHandler: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("TitleSummarizer", () => {
|
||||
const mockApiConfiguration: ProviderSettings = {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-key",
|
||||
apiModelId: "claude-3-opus-20240229",
|
||||
}
|
||||
|
||||
const mockListApiConfigMeta: ProviderSettingsEntry[] = [
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
apiProvider: "anthropic",
|
||||
},
|
||||
{
|
||||
id: "enhancement",
|
||||
name: "Enhancement",
|
||||
apiProvider: "openai",
|
||||
},
|
||||
]
|
||||
|
||||
const mockProviderSettingsManager = {
|
||||
getProfile: vi.fn().mockResolvedValue({
|
||||
id: "enhancement",
|
||||
name: "Enhancement",
|
||||
apiProvider: "openai",
|
||||
openAiApiKey: "test-openai-key",
|
||||
openAiModelId: "gpt-4",
|
||||
}),
|
||||
} as any // Mock the ProviderSettingsManager type
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Set default mock behavior
|
||||
vi.mocked(singleCompletionHandler).mockResolvedValue("Short concise title")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe("summarizeTitle", () => {
|
||||
it("should successfully summarize a long title", async () => {
|
||||
const longText =
|
||||
"I need help implementing a comprehensive user authentication system with OAuth2 support for Google, Facebook, and GitHub providers, including secure session management, password reset functionality, email verification, two-factor authentication, and proper error handling with rate limiting to prevent brute force attacks"
|
||||
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: longText,
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
maxLength: 150,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.summarizedTitle).toBe("Short concise title")
|
||||
expect(result.summarizedTitle!.length).toBeLessThan(longText.length)
|
||||
})
|
||||
|
||||
it("should return original text if it's already short", async () => {
|
||||
const shortText = "Fix bug in login"
|
||||
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: shortText,
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
maxLength: 150,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
// Text is already shorter than max length, so it returns as-is
|
||||
expect(result.summarizedTitle).toBe(shortText)
|
||||
})
|
||||
|
||||
it("should use enhancement API configuration when provided", async () => {
|
||||
const longText =
|
||||
"This is a very long title that definitely needs summarization to be more concise and readable for users, making it easier to understand the main point of the task at hand"
|
||||
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: longText,
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
enhancementApiConfigId: "enhancement",
|
||||
listApiConfigMeta: mockListApiConfigMeta,
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
maxLength: 150,
|
||||
})
|
||||
|
||||
// The function will call providerSettingsManager.getProfile if all conditions are met
|
||||
expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "enhancement" })
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.summarizedTitle).toBe("Short concise title")
|
||||
})
|
||||
|
||||
it("should handle API errors gracefully", async () => {
|
||||
const longText =
|
||||
"This is a very long text that definitely needs summarization to be more concise and readable for users, making it easier to understand the main point of the task at hand"
|
||||
vi.mocked(singleCompletionHandler).mockRejectedValueOnce(new Error("API Error"))
|
||||
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: longText,
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
maxLength: 150,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe("API Error")
|
||||
expect(result.summarizedTitle).toBe(longText)
|
||||
})
|
||||
|
||||
it("should handle missing API configuration", async () => {
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: "Some text",
|
||||
apiConfiguration: undefined as any,
|
||||
maxLength: 150,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe("No API configuration available")
|
||||
expect(result.summarizedTitle).toBe("Some text")
|
||||
})
|
||||
|
||||
it("should respect custom maxLength parameter", async () => {
|
||||
const shortText = "Short text"
|
||||
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: shortText,
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
maxLength: 100,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
// Text is already shorter than maxLength, returns as-is
|
||||
expect(result.summarizedTitle).toBe(shortText)
|
||||
})
|
||||
|
||||
it("should use custom support prompts when provided", async () => {
|
||||
const customPrompts = {
|
||||
SUMMARIZE_TITLE: "Custom summarization prompt: {{userInput}} (max 150 chars)",
|
||||
}
|
||||
|
||||
// Short text doesn't need summarization
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: "Text to summarize",
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
customSupportPrompts: customPrompts,
|
||||
maxLength: 150,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.summarizedTitle).toBe("Text to summarize")
|
||||
})
|
||||
|
||||
it("should handle empty response from API", async () => {
|
||||
const longText =
|
||||
"This is a very long text that definitely needs summarization to be more concise and readable for users, making it easier to understand the main point of the task at hand"
|
||||
vi.mocked(singleCompletionHandler).mockResolvedValueOnce("")
|
||||
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: longText,
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
maxLength: 150,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe("Received empty summarized title")
|
||||
expect(result.summarizedTitle).toBe(longText)
|
||||
})
|
||||
|
||||
it("should trim whitespace from summarized title", async () => {
|
||||
const longText =
|
||||
"This is a very long text that definitely needs summarization to be more concise and readable for users, making it easier to understand the main point of the task at hand"
|
||||
vi.mocked(singleCompletionHandler).mockResolvedValueOnce(" Trimmed title \n")
|
||||
|
||||
const result = await TitleSummarizer.summarizeTitle({
|
||||
text: longText,
|
||||
apiConfiguration: mockApiConfiguration,
|
||||
maxLength: 150,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.summarizedTitle).toBe("Trimmed title")
|
||||
})
|
||||
})
|
||||
|
||||
describe("captureTelemetry", () => {
|
||||
it("should not capture telemetry events (currently disabled)", () => {
|
||||
const taskId = "test-task-123"
|
||||
const originalLength = 250
|
||||
const summarizedLength = 100
|
||||
|
||||
TitleSummarizer.captureTelemetry(taskId, originalLength, summarizedLength)
|
||||
|
||||
// Since telemetry is commented out in the implementation, it should not be called
|
||||
expect(TelemetryService.instance.captureEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
125
src/core/webview/titleSummarizer.ts
Normal file
125
src/core/webview/titleSummarizer.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { ProviderSettings, ClineMessage, TelemetryEventName } from "@roo-code/types"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
import { supportPrompt } from "../../shared/support-prompt"
|
||||
import { singleCompletionHandler } from "../../utils/single-completion-handler"
|
||||
import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
|
||||
import { ClineProvider } from "./ClineProvider"
|
||||
|
||||
export interface TitleSummarizerOptions {
|
||||
text: string
|
||||
apiConfiguration: ProviderSettings
|
||||
customSupportPrompts?: Record<string, any>
|
||||
listApiConfigMeta?: Array<{ id: string; name?: string }>
|
||||
enhancementApiConfigId?: string
|
||||
providerSettingsManager?: ProviderSettingsManager
|
||||
maxLength?: number
|
||||
}
|
||||
|
||||
export interface TitleSummarizerResult {
|
||||
success: boolean
|
||||
summarizedTitle?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarizes long task titles using AI, similar to message enhancement
|
||||
*/
|
||||
export class TitleSummarizer {
|
||||
/**
|
||||
* Summarizes a task title using the configured AI provider
|
||||
* @param options Configuration options for title summarization
|
||||
* @returns Summarized title result with success status
|
||||
*/
|
||||
static async summarizeTitle(options: TitleSummarizerOptions): Promise<TitleSummarizerResult> {
|
||||
try {
|
||||
const {
|
||||
text,
|
||||
apiConfiguration,
|
||||
customSupportPrompts,
|
||||
listApiConfigMeta,
|
||||
enhancementApiConfigId,
|
||||
providerSettingsManager,
|
||||
maxLength = 150,
|
||||
} = options
|
||||
|
||||
// Check if title needs summarization
|
||||
if (text.length <= maxLength) {
|
||||
return {
|
||||
success: true,
|
||||
summarizedTitle: text,
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which API configuration to use
|
||||
let configToUse: ProviderSettings = apiConfiguration
|
||||
|
||||
// Try to get enhancement config first, fall back to current config
|
||||
if (
|
||||
enhancementApiConfigId &&
|
||||
listApiConfigMeta?.find(({ id }) => id === enhancementApiConfigId) &&
|
||||
providerSettingsManager
|
||||
) {
|
||||
const { name: _, ...providerSettings } = await providerSettingsManager.getProfile({
|
||||
id: enhancementApiConfigId,
|
||||
})
|
||||
|
||||
if (providerSettings.apiProvider) {
|
||||
configToUse = providerSettings
|
||||
}
|
||||
}
|
||||
|
||||
// Create the summarization prompt using the support prompt system
|
||||
const summarizationPrompt = supportPrompt.create(
|
||||
"SUMMARIZE_TITLE",
|
||||
{ userInput: text },
|
||||
customSupportPrompts,
|
||||
)
|
||||
|
||||
// Call the single completion handler to get the summarized title
|
||||
const summarizedTitle = await singleCompletionHandler(configToUse, summarizationPrompt)
|
||||
|
||||
// Validate the summarized title
|
||||
if (!summarizedTitle || summarizedTitle.trim().length === 0) {
|
||||
throw new Error("Received empty summarized title")
|
||||
}
|
||||
|
||||
// Ensure the summarized title doesn't exceed the max length
|
||||
const trimmedTitle = summarizedTitle.trim()
|
||||
if (trimmedTitle.length > maxLength) {
|
||||
// If the AI didn't respect the length limit, truncate manually
|
||||
return {
|
||||
success: true,
|
||||
summarizedTitle: trimmedTitle.substring(0, maxLength - 3) + "...",
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
summarizedTitle: trimmedTitle,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
summarizedTitle: options.text, // Return original text as fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures telemetry for title summarization
|
||||
* @param taskId Optional task ID for telemetry tracking
|
||||
* @param originalLength Length of the original title
|
||||
* @param summarizedLength Length of the summarized title
|
||||
*/
|
||||
static captureTelemetry(taskId?: string, originalLength?: number, summarizedLength?: number): void {
|
||||
// TODO: Add telemetry event for title summarization when available
|
||||
// if (TelemetryService.hasInstance()) {
|
||||
// TelemetryService.instance.captureEvent(TelemetryEventName.TASK_TITLE_SUMMARIZED, {
|
||||
// ...(taskId && { taskId }),
|
||||
// originalLength: originalLength ?? 0,
|
||||
// summarizedLength: summarizedLength ?? 0,
|
||||
// })
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ type SupportPromptType =
|
|||
| "TERMINAL_FIX"
|
||||
| "TERMINAL_EXPLAIN"
|
||||
| "NEW_TASK"
|
||||
| "SUMMARIZE_TITLE"
|
||||
|
||||
const supportPromptConfigs: Record<SupportPromptType, SupportPromptConfig> = {
|
||||
ENHANCE: {
|
||||
|
|
@ -174,6 +175,19 @@ Please provide:
|
|||
NEW_TASK: {
|
||||
template: `\${userInput}`,
|
||||
},
|
||||
SUMMARIZE_TITLE: {
|
||||
template: `Create a clear, concise title (under 150 characters) that captures the essence of this task:
|
||||
|
||||
<task_description>
|
||||
\${userInput}
|
||||
</task_description>
|
||||
|
||||
IMPORTANT:
|
||||
- Keep it under 150 characters
|
||||
- Be specific and descriptive
|
||||
- No quotes or special formatting
|
||||
- Reply with ONLY the title, nothing else`,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const supportPrompt = {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue