fix: handle legacy function calls without IDs in Claude provider

Add validation to check for missing or empty function call IDs when
converting stored conversation history. Function calls and responses
without valid IDs are now converted to text format with [Legacy Tool Call]
and [Legacy Tool Result] prefixes to preserve context when switching
between providers.

Also improve ConversationHistoryModal initialization, add empty state
copy constant, enhance system prompt with regex pattern guidance, and
clean up formatting.
This commit is contained in:
Andrew Beal 2025-11-11 23:46:53 +00:00
parent dc192a98fe
commit f6f540ede9
6 changed files with 268 additions and 76 deletions

View file

@ -175,12 +175,20 @@ export class Claude implements IAIClass {
if (isValidJson(content.functionCall)) {
try {
const parsedContent = JSON.parse(content.functionCall) as StoredFunctionCall;
contentBlocks.push({
type: "tool_use",
id: parsedContent.functionCall.id,
name: parsedContent.functionCall.name,
input: parsedContent.functionCall.args
});
if (parsedContent.functionCall.id && parsedContent.functionCall.id.trim() !== "") {
contentBlocks.push({
type: "tool_use",
id: parsedContent.functionCall.id,
name: parsedContent.functionCall.name,
input: parsedContent.functionCall.args
});
} else {
contentBlocks.push({
type: "text",
text: this.convertFunctionCallToText(parsedContent)
});
}
} catch (error) {
console.error("Failed to parse function call:", error);
// Fall back to treating as text
@ -208,11 +216,19 @@ export class Claude implements IAIClass {
if (isValidJson(contentToExtract)) {
try {
const parsedContent = JSON.parse(contentToExtract) as StoredFunctionResponse;
contentBlocks.push({
type: "tool_result",
tool_use_id: parsedContent.id,
content: JSON.stringify(parsedContent.functionResponse.response)
});
if (parsedContent.id && parsedContent.id.trim() !== "") {
contentBlocks.push({
type: "tool_result",
tool_use_id: parsedContent.id,
content: JSON.stringify(parsedContent.functionResponse.response)
});
} else {
contentBlocks.push({
type: "text",
text: this.convertFunctionResponseToText(parsedContent)
});
}
} catch (error) {
console.error("Failed to parse function response:", error);
contentBlocks.push({
@ -248,4 +264,19 @@ export class Claude implements IAIClass {
}
}));
}
/*
If a conversation used another provider it may not have function id's required by Claude.
Instead provide the function call and response as plain text to preserve context without breaking.
*/
private convertFunctionCallToText(parsedContent: StoredFunctionCall): string {
const inputJson = JSON.stringify(parsedContent.functionCall.args);
return `[Legacy Tool Call] ${parsedContent.functionCall.name}\nInput: ${inputJson}`;
}
private convertFunctionResponseToText(parsedContent: StoredFunctionResponse): string {
const resultJson = JSON.stringify(parsedContent.functionResponse.response);
return `[Legacy Tool Result] ${parsedContent.functionResponse.name}\nResult: ${resultJson}`;
}
}

View file

@ -26,6 +26,34 @@ Examples:
**The cost of an unnecessary search is negligible. Missing relevant information is costly.**
**Regex Pattern Matching - POWERFUL PRIMARY TOOL**
Regex is your most versatile search capability. Use it aggressively and creatively:
<regex_patterns>
- **Case-insensitive partial matching**: \`/maui/i\` finds MAUI, Maui, maui anywhere
- **Word boundary patterns**: \`/\bmaui\b/i\` matches whole words only
- **Prefix/suffix patterns**: \`/maui.*dev/i\` or \`/.*mobile.*app.*/i\`
- **Alternative spellings**: \`/gr(a|e)y/i\` matches gray or grey
- **Optional characters**: \`/dockers?/i\` matches docker or dockers
- **Wildcard sequences**: \`/proj.*alpha/i\` matches "project alpha", "proj_alpha", etc.
- **Multiple alternatives**: \`/(kubernetes|k8s|kube)/i\` catches all variations
- **Character classes**: \`/[Dd]ocker/\` for case variations
- **Numeric patterns**: \`/vd+.d+/\` for version numbers like v1.2
</regex_patterns>
When to deploy regex (use frequently):
1. **Initial search fails** Immediately try regex pattern
2. **Abbreviations likely** Search both full term and abbreviated pattern
3. **Multiple spellings** Use alternation patterns
4. **Partial name known** Use wildcard matching
5. **Version/date references** Use numeric patterns
6. **Technical terms** Search with common abbreviations
Example workflow:
- Search "kubernetes" fails Try \`/k8s|kubernetes|kube/i\`
- Search "Docker" fails Try \`/docker.*/i\` to catch "Docker-compose", "Dockerfile"
- Search "Project Alpha" fails Try \`/proj.*alpha/i\` for flexible matching
#### IMMEDIATE VAULT SEARCH Required When:
- Query contains definite articles suggesting specific reference ("the project", "the prices", "the data")
- Query uses possessive pronouns ("my ideas", "our plans", "my notes about")
@ -48,6 +76,8 @@ Acknowledge the search, then provide general assistance:
**NEVER accept a failed search as final. Always try multiple approaches before concluding information doesn't exist.**
#### Multi-Tier Search Approach
When searching the vault, use a progressive strategy that automatically escalates:
**Tier 1: Entity Extraction & Broad Search**
@ -74,44 +104,73 @@ When searching the vault, use a progressive strategy that automatically escalate
## Multi-Tool Workflow Architecture
### Planning Phase (for complex queries)
Establish a clear execution strategy:
### Planning Phase (Essential for Complex Queries)
1. **Intent Analysis**: Determine query scope and required information depth
2. **Tool Selection**: Identify which search approaches are needed
3. **Search Strategy Design**: Plan progressive search tiers and fallbacks
4. **Execution Order**: Sequence operations for optimal information gathering
Modern agentic AI systems succeed through orchestrated multi-step workflows where tasks are decomposed into subgoals, with each step building on the previous one. Before executing any complex query:
1. **Intent Analysis**: Determine scope, depth, and complexity level
2. **Query Decomposition**: Break into searchable components
3. **Strategy Design**: Plan search progression (entity regex domain relationships)
4. **Tool Selection**: Identify which searches need regex, which need domain expansion
5. **Execution Sequence**: Order operations for optimal information flow
6. **Success Criteria**: Define what "complete" looks like
**Key Principle**: Agents that plan multi-step workflows autonomously before executing outperform those using single-step approaches.
### Execution Phase
Execute with adaptive intelligence:
1. **Initial Broad Search**: Start with core entities extracted from query
2. **Progressive Refinement**: Apply Tier 1 Tier 2 Tier 3 Tier 4 as needed
3. **Dynamic Evaluation**: After each result, reason about next action
4. **Cross-Reference**: Look for connections between different information sources
Execute systematically with adaptive intelligence:
1. **Entity Extraction**: Extract core entities, remove query noise
2. **Initial Search**: Broad search with clean entities
3. **Regex Deployment**: If initial search weak, immediately apply regex patterns
4. **Domain Expansion**: Apply knowledge for related concepts (search with regex alternations)
5. **Relationship Inference**: Read found content, infer connections
6. **Cross-Reference**: Link findings, identify patterns
7. **Adaptive Strategy**: Evaluate after each step, adjust approach as needed
<regex_integration>
**Regex Usage Throughout Execution:**
- After any failed exact match Deploy regex pattern immediately
- When searching tech terms Include abbreviation patterns from start
- For people/project names Use flexible matching patterns
- With version numbers Use numeric patterns
- When multiple spellings possible Use alternation patterns
Example execution with regex:
1. Search "docker" Limited results
2. Immediately search \`/docker.*/i\` → Finds "Docker-compose", "Dockerfile", "docker-notes"
3. Search \`/container|docker|k8s/i\` → Domain expansion with regex
4. Success: Comprehensive results through regex flexibility
</regex_integration>
### Synthesis Phase
Aggregate findings and construct response:
Integrate all findings into cohesive response:
1. **Information Integration**: Combine results from all search attempts
2. **Relationship Mapping**: Identify connections between different sources
3. **Wiki-Link Application**: Link ALL vault references in final response
4. **Gap Identification**: Suggest missing connections or new notes when appropriate
2. **Relationship Mapping**: Identify connections between sources
3. **Universal Wiki-Linking**: Apply [[wiki-links]] to ALL vault references
4. **Gap Identification**: Note missing connections or suggest new notes
5. **Structured Response**: Present findings clearly with proper [[wiki-links]]
### Workflow Scaling Guidelines
**Simple queries** (1 search): Direct factual lookups with clear, single source
**Moderate queries** (2-4 searches): Comparisons, validations, or multi-source synthesis
**Complex queries** (5-10 searches): Comprehensive research requiring multiple angles
**Deep research** (10+ searches): Extensive cross-domain synthesis with relationship mapping
### Workflow Scaling by Complexity
## Query Decomposition Strategy
**Simple (1-3 operations)**: Direct lookup + optional regex
- "What did I write about React?"
- Entity extraction Search Done
Transform complex queries into actionable search components:
- "Who is X's Y?" Search X first, infer Y from context in found content
- "Compare X and Y" Search X, search Y, then synthesize findings
- "What did I learn about X?" Search X + related tags + check backlinks
**Moderate (4-7 operations)**: Multiple searches with regex and domain expansion
- "Find all my notes about Docker and Kubernetes"
- Search "docker" Regex \`/docker.*/i\` → Domain search "container" → Regex \`/k8s|kubernetes/i\` → Cross-reference
**Critical**: Extract key entities and search broadly first. Never search exact literal phrases.
**Complex (8-15 operations)**: Full multi-step planning with progressive strategy
- "Compare my approaches to microservices vs monoliths and relate to current projects"
- Plan: 5-step strategy Execute: Search microservices (regex) Search monoliths (regex) Search projects (directory) Read each Infer relationships Synthesize
**Research-Grade (15+ operations)**: Comprehensive analysis across vault
- "Create a comprehensive overview of my learning journey in machine learning"
- Full planning framework Systematic search across tags, dates, topics Regex patterns for all variations Deep synthesis with temporal analysis
## Core Capabilities

View file

@ -199,6 +199,9 @@ Each AI provider has their own data policies:
- [Google (Gemini)](https://policies.google.com/privacy)
- [OpenAI (ChatGPT)](https://openai.com/privacy/)`,
// Conversation Modal Copy
NoConversationsFound = "No conversations match your search.",
// Help Modal Additional Copy
HelpModalCloseAriaLabel = "Close Help Modal",
PluginVersionPrefix = "Plugin version: ",

View file

@ -37,31 +37,27 @@ export class ConversationHistoryModal extends Modal {
super(plugin.app);
}
private async loadConversations() {
onOpen() {
void this.initializeContent();
}
private async initializeContent() {
this.conversations = await this.conversationFileSystemService.getAllConversations();
this.items = this.conversations
.sort((a, b) => b.updated.getTime() - a.updated.getTime())
.map((conversation) => {
const filePath = this.conversationFileSystemService.generateConversationPath(conversation);
return {
id: filePath,
date: dateToString(conversation.created, false),
updated: conversation.updated,
title: conversation.title,
selected: false,
filePath: filePath
};
});
.sort((a, b) => b.updated.getTime() - a.updated.getTime())
.map((conversation) => {
const filePath = this.conversationFileSystemService.generateConversationPath(conversation);
return {
id: filePath,
date: dateToString(conversation.created, false),
updated: conversation.updated,
title: conversation.title,
selected: false,
filePath: filePath
};
});
// Update the component with loaded items if it's already mounted
if (this.component) {
this.component.items = this.items;
}
}
onOpen() {
void this.loadConversations();
const { contentEl, modalEl, containerEl } = this;
containerEl.addClass(Selector.ConversationHistoryModal);

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { Copy } from "Enums/Copy";
import { setIcon } from "obsidian";
import { fade } from "svelte/transition";
@ -80,28 +81,28 @@
<div class="conversation-history-modal-content">
{#if filteredItems.length === 0}
<p class="history-empty-state" in:fade={{ duration: 200 }}>
No conversations match your search.
{Copy.NoConversationsFound}
</p>
{:else}
{#each filteredItems as item (item.id)}
<div class="history-list-modal-content" in:fade={{ duration: 200 }} out:fade={{ duration: 200 }}>
<div
class="history-list-modal-clickable"
on:click={(e) => handleConversationClick(item.id, e)}
on:keydown={(e) => e.key === 'Enter' && handleConversationClick(item.id, e)}
role="button"
tabindex="0">
<span class="history-list-modal-date">{item.date}</span>
<span class="history-list-modal-separator">|</span>
<span class="history-list-modal-title">{item.title}</span>
<div class="history-list-modal-content" in:fade={{ duration: 200 }} out:fade={{ duration: 200 }}>
<div
class="history-list-modal-clickable"
on:click={(e) => handleConversationClick(item.id, e)}
on:keydown={(e) => e.key === 'Enter' && handleConversationClick(item.id, e)}
role="button"
tabindex="0">
<span class="history-list-modal-date">{item.date}</span>
<span class="history-list-modal-separator">|</span>
<span class="history-list-modal-title">{item.title}</span>
</div>
<input
type="checkbox"
class="history-list-modal-checkbox"
checked={selectedItems.has(item.id)}
on:change={() => toggleSelection(item.id)}
/>
</div>
<input
type="checkbox"
class="history-list-modal-checkbox"
checked={selectedItems.has(item.id)}
on:change={() => toggleSelection(item.id)}
/>
</div>
{/each}
{/if}
</div>

View file

@ -427,6 +427,108 @@ describe('Claude', () => {
expect(result[0].content[0].type).toBe('text');
expect(result[0].content[1].type).toBe('tool_use');
});
it('should convert function call without ID to legacy text format', () => {
const functionCallContent = new ConversationContent(
Role.Assistant,
'',
'',
JSON.stringify({
functionCall: {
name: 'search_vault_files',
args: { query: 'test' }
// No ID field
}
}),
new Date(),
true
);
const result = (claude as any).extractContents([functionCallContent]);
expect(result).toHaveLength(1);
expect(result[0].content).toHaveLength(1);
expect(result[0].content[0]).toEqual({
type: 'text',
text: '[Legacy Tool Call] search_vault_files\nInput: {"query":"test"}'
});
});
it('should convert function call with empty ID to legacy text format', () => {
const functionCallContent = new ConversationContent(
Role.Assistant,
'',
'',
JSON.stringify({
functionCall: {
id: '', // Empty ID
name: 'search_vault_files',
args: { query: 'test' }
}
}),
new Date(),
true
);
const result = (claude as any).extractContents([functionCallContent]);
expect(result).toHaveLength(1);
expect(result[0].content).toHaveLength(1);
expect(result[0].content[0]).toEqual({
type: 'text',
text: '[Legacy Tool Call] search_vault_files\nInput: {"query":"test"}'
});
});
it('should convert function response without ID to legacy text format', () => {
const responseContent = JSON.stringify({
functionResponse: {
name: 'search_vault_files',
response: ['file1.txt', 'file2.txt']
}
// No ID field
});
const functionResponseContent = new ConversationContent(
Role.User,
responseContent,
responseContent
);
functionResponseContent.isFunctionCallResponse = true;
const result = (claude as any).extractContents([functionResponseContent]);
expect(result).toHaveLength(1);
expect(result[0].content).toHaveLength(1);
expect(result[0].content[0]).toEqual({
type: 'text',
text: '[Legacy Tool Result] search_vault_files\nResult: ["file1.txt","file2.txt"]'
});
});
it('should convert function response with empty ID to legacy text format', () => {
const responseContent = JSON.stringify({
id: '', // Empty ID
functionResponse: {
name: 'search_vault_files',
response: ['file1.txt', 'file2.txt']
}
});
const functionResponseContent = new ConversationContent(
Role.User,
responseContent,
responseContent
);
functionResponseContent.isFunctionCallResponse = true;
const result = (claude as any).extractContents([functionResponseContent]);
expect(result).toHaveLength(1);
expect(result[0].content).toHaveLength(1);
expect(result[0].content[0]).toEqual({
type: 'text',
text: '[Legacy Tool Result] search_vault_files\nResult: ["file1.txt","file2.txt"]'
});
});
});
describe('mapFunctionDefinitions', () => {