forked from cardosofelipe/fast-next-template
feat(context): implement model adapters for Claude and OpenAI (#83)
Phase 5 of Context Management Engine - Model Adapters: - Add ModelAdapter abstract base class with model matching - Add DefaultAdapter for unknown models (plain text) - Add ClaudeAdapter with XML-based formatting: - <system_instructions> for system context - <reference_documents>/<document> for knowledge - <conversation_history>/<message> for chat - <tool_results>/<tool_result> for tool outputs - XML escaping for special characters - Add OpenAIAdapter with markdown formatting: - ## headers for sections - ### Source headers for documents - **ROLE** bold labels for conversation - Code blocks for tool outputs - Add get_adapter() factory function for model selection Tests: 33 new tests, 256 total context tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,15 @@ from .compression import (
|
|||||||
TruncationStrategy,
|
TruncationStrategy,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Adapters
|
||||||
|
from .adapters import (
|
||||||
|
ClaudeAdapter,
|
||||||
|
DefaultAdapter,
|
||||||
|
get_adapter,
|
||||||
|
ModelAdapter,
|
||||||
|
OpenAIAdapter,
|
||||||
|
)
|
||||||
|
|
||||||
# Prioritization
|
# Prioritization
|
||||||
from .prioritization import (
|
from .prioritization import (
|
||||||
ContextRanker,
|
ContextRanker,
|
||||||
@@ -110,6 +119,12 @@ from .types import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
# Adapters
|
||||||
|
"ClaudeAdapter",
|
||||||
|
"DefaultAdapter",
|
||||||
|
"get_adapter",
|
||||||
|
"ModelAdapter",
|
||||||
|
"OpenAIAdapter",
|
||||||
# Assembly
|
# Assembly
|
||||||
"ContextPipeline",
|
"ContextPipeline",
|
||||||
"PipelineMetrics",
|
"PipelineMetrics",
|
||||||
|
|||||||
@@ -1,5 +1,35 @@
|
|||||||
"""
|
"""
|
||||||
Model Adapters Module.
|
Model Adapters Module.
|
||||||
|
|
||||||
Provides model-specific context formatting.
|
Provides model-specific context formatting adapters.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .base import DefaultAdapter, ModelAdapter
|
||||||
|
from .claude import ClaudeAdapter
|
||||||
|
from .openai import OpenAIAdapter
|
||||||
|
|
||||||
|
|
||||||
|
def get_adapter(model: str) -> ModelAdapter:
|
||||||
|
"""
|
||||||
|
Get the appropriate adapter for a model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Model name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Adapter instance for the model
|
||||||
|
"""
|
||||||
|
if ClaudeAdapter.matches_model(model):
|
||||||
|
return ClaudeAdapter()
|
||||||
|
elif OpenAIAdapter.matches_model(model):
|
||||||
|
return OpenAIAdapter()
|
||||||
|
return DefaultAdapter()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ClaudeAdapter",
|
||||||
|
"DefaultAdapter",
|
||||||
|
"get_adapter",
|
||||||
|
"ModelAdapter",
|
||||||
|
"OpenAIAdapter",
|
||||||
|
]
|
||||||
|
|||||||
178
backend/app/services/context/adapters/base.py
Normal file
178
backend/app/services/context/adapters/base.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
"""
|
||||||
|
Base Model Adapter.
|
||||||
|
|
||||||
|
Abstract base class for model-specific context formatting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..types import BaseContext, ContextType
|
||||||
|
|
||||||
|
|
||||||
|
class ModelAdapter(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base adapter for model-specific context formatting.
|
||||||
|
|
||||||
|
Each adapter knows how to format contexts for optimal
|
||||||
|
understanding by a specific LLM family (Claude, OpenAI, etc.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Model name patterns this adapter handles
|
||||||
|
MODEL_PATTERNS: list[str] = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def matches_model(cls, model: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if this adapter handles the given model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model: Model name to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if this adapter handles the model
|
||||||
|
"""
|
||||||
|
model_lower = model.lower()
|
||||||
|
return any(pattern in model_lower for pattern in cls.MODEL_PATTERNS)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def format(
|
||||||
|
self,
|
||||||
|
contexts: list[BaseContext],
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format contexts for the target model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contexts: List of contexts to format
|
||||||
|
**kwargs: Additional formatting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted context string
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def format_type(
|
||||||
|
self,
|
||||||
|
contexts: list[BaseContext],
|
||||||
|
context_type: ContextType,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format contexts of a specific type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contexts: List of contexts of the same type
|
||||||
|
context_type: The type of contexts
|
||||||
|
**kwargs: Additional formatting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted string for this context type
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_type_order(self) -> list[ContextType]:
|
||||||
|
"""
|
||||||
|
Get the preferred order of context types.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of context types in preferred order
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
ContextType.SYSTEM,
|
||||||
|
ContextType.TASK,
|
||||||
|
ContextType.KNOWLEDGE,
|
||||||
|
ContextType.CONVERSATION,
|
||||||
|
ContextType.TOOL,
|
||||||
|
]
|
||||||
|
|
||||||
|
def group_by_type(
|
||||||
|
self, contexts: list[BaseContext]
|
||||||
|
) -> dict[ContextType, list[BaseContext]]:
|
||||||
|
"""
|
||||||
|
Group contexts by their type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contexts: List of contexts to group
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping context type to list of contexts
|
||||||
|
"""
|
||||||
|
by_type: dict[ContextType, list[BaseContext]] = {}
|
||||||
|
for context in contexts:
|
||||||
|
ct = context.get_type()
|
||||||
|
if ct not in by_type:
|
||||||
|
by_type[ct] = []
|
||||||
|
by_type[ct].append(context)
|
||||||
|
return by_type
|
||||||
|
|
||||||
|
def get_separator(self) -> str:
|
||||||
|
"""
|
||||||
|
Get the separator between context sections.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Separator string
|
||||||
|
"""
|
||||||
|
return "\n\n"
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultAdapter(ModelAdapter):
|
||||||
|
"""
|
||||||
|
Default adapter for unknown models.
|
||||||
|
|
||||||
|
Uses simple plain-text formatting with minimal structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MODEL_PATTERNS: list[str] = [] # Fallback adapter
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def matches_model(cls, model: str) -> bool:
|
||||||
|
"""Always returns True as fallback."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def format(
|
||||||
|
self,
|
||||||
|
contexts: list[BaseContext],
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
"""Format contexts as plain text."""
|
||||||
|
if not contexts:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
by_type = self.group_by_type(contexts)
|
||||||
|
parts: list[str] = []
|
||||||
|
|
||||||
|
for ct in self.get_type_order():
|
||||||
|
if ct in by_type:
|
||||||
|
formatted = self.format_type(by_type[ct], ct, **kwargs)
|
||||||
|
if formatted:
|
||||||
|
parts.append(formatted)
|
||||||
|
|
||||||
|
return self.get_separator().join(parts)
|
||||||
|
|
||||||
|
def format_type(
|
||||||
|
self,
|
||||||
|
contexts: list[BaseContext],
|
||||||
|
context_type: ContextType,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
"""Format contexts of a type as plain text."""
|
||||||
|
if not contexts:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
content = "\n\n".join(c.content for c in contexts)
|
||||||
|
|
||||||
|
if context_type == ContextType.SYSTEM:
|
||||||
|
return content
|
||||||
|
elif context_type == ContextType.TASK:
|
||||||
|
return f"Task:\n{content}"
|
||||||
|
elif context_type == ContextType.KNOWLEDGE:
|
||||||
|
return f"Reference Information:\n{content}"
|
||||||
|
elif context_type == ContextType.CONVERSATION:
|
||||||
|
return f"Previous Conversation:\n{content}"
|
||||||
|
elif context_type == ContextType.TOOL:
|
||||||
|
return f"Tool Results:\n{content}"
|
||||||
|
|
||||||
|
return content
|
||||||
178
backend/app/services/context/adapters/claude.py
Normal file
178
backend/app/services/context/adapters/claude.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
"""
|
||||||
|
Claude Model Adapter.
|
||||||
|
|
||||||
|
Provides Claude-specific context formatting using XML tags
|
||||||
|
which Claude models understand natively.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..types import BaseContext, ContextType
|
||||||
|
from .base import ModelAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class ClaudeAdapter(ModelAdapter):
|
||||||
|
"""
|
||||||
|
Claude-specific context formatting adapter.
|
||||||
|
|
||||||
|
Claude models have native understanding of XML structure,
|
||||||
|
so we use XML tags for clear delineation of context types.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- XML tags for each context type
|
||||||
|
- Document structure for knowledge contexts
|
||||||
|
- Role-based message formatting for conversations
|
||||||
|
- Tool result wrapping with tool names
|
||||||
|
"""
|
||||||
|
|
||||||
|
MODEL_PATTERNS: list[str] = ["claude", "anthropic"]
|
||||||
|
|
||||||
|
def format(
|
||||||
|
self,
|
||||||
|
contexts: list[BaseContext],
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format contexts for Claude models.
|
||||||
|
|
||||||
|
Uses XML tags for structured content that Claude
|
||||||
|
understands natively.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contexts: List of contexts to format
|
||||||
|
**kwargs: Additional formatting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
XML-structured context string
|
||||||
|
"""
|
||||||
|
if not contexts:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
by_type = self.group_by_type(contexts)
|
||||||
|
parts: list[str] = []
|
||||||
|
|
||||||
|
for ct in self.get_type_order():
|
||||||
|
if ct in by_type:
|
||||||
|
formatted = self.format_type(by_type[ct], ct, **kwargs)
|
||||||
|
if formatted:
|
||||||
|
parts.append(formatted)
|
||||||
|
|
||||||
|
return self.get_separator().join(parts)
|
||||||
|
|
||||||
|
def format_type(
|
||||||
|
self,
|
||||||
|
contexts: list[BaseContext],
|
||||||
|
context_type: ContextType,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format contexts of a specific type for Claude.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contexts: List of contexts of the same type
|
||||||
|
context_type: The type of contexts
|
||||||
|
**kwargs: Additional formatting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
XML-formatted string for this context type
|
||||||
|
"""
|
||||||
|
if not contexts:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if context_type == ContextType.SYSTEM:
|
||||||
|
return self._format_system(contexts)
|
||||||
|
elif context_type == ContextType.TASK:
|
||||||
|
return self._format_task(contexts)
|
||||||
|
elif context_type == ContextType.KNOWLEDGE:
|
||||||
|
return self._format_knowledge(contexts)
|
||||||
|
elif context_type == ContextType.CONVERSATION:
|
||||||
|
return self._format_conversation(contexts)
|
||||||
|
elif context_type == ContextType.TOOL:
|
||||||
|
return self._format_tool(contexts)
|
||||||
|
|
||||||
|
return "\n".join(c.content for c in contexts)
|
||||||
|
|
||||||
|
def _format_system(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""Format system contexts."""
|
||||||
|
content = "\n\n".join(c.content for c in contexts)
|
||||||
|
return f"<system_instructions>\n{content}\n</system_instructions>"
|
||||||
|
|
||||||
|
def _format_task(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""Format task contexts."""
|
||||||
|
content = "\n\n".join(c.content for c in contexts)
|
||||||
|
return f"<current_task>\n{content}\n</current_task>"
|
||||||
|
|
||||||
|
def _format_knowledge(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""
|
||||||
|
Format knowledge contexts as structured documents.
|
||||||
|
|
||||||
|
Each knowledge context becomes a document with source attribution.
|
||||||
|
"""
|
||||||
|
parts = ["<reference_documents>"]
|
||||||
|
|
||||||
|
for ctx in contexts:
|
||||||
|
source = self._escape_xml(ctx.source)
|
||||||
|
content = ctx.content
|
||||||
|
score = ctx.metadata.get("score", ctx.metadata.get("relevance_score", ""))
|
||||||
|
|
||||||
|
if score:
|
||||||
|
parts.append(f'<document source="{source}" relevance="{score}">')
|
||||||
|
else:
|
||||||
|
parts.append(f'<document source="{source}">')
|
||||||
|
|
||||||
|
parts.append(content)
|
||||||
|
parts.append("</document>")
|
||||||
|
|
||||||
|
parts.append("</reference_documents>")
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
def _format_conversation(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""
|
||||||
|
Format conversation contexts as message history.
|
||||||
|
|
||||||
|
Uses role-based message tags for clear turn delineation.
|
||||||
|
"""
|
||||||
|
parts = ["<conversation_history>"]
|
||||||
|
|
||||||
|
for ctx in contexts:
|
||||||
|
role = ctx.metadata.get("role", "user")
|
||||||
|
parts.append(f'<message role="{role}">')
|
||||||
|
parts.append(ctx.content)
|
||||||
|
parts.append("</message>")
|
||||||
|
|
||||||
|
parts.append("</conversation_history>")
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
def _format_tool(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""
|
||||||
|
Format tool contexts as tool results.
|
||||||
|
|
||||||
|
Each tool result is wrapped with the tool name.
|
||||||
|
"""
|
||||||
|
parts = ["<tool_results>"]
|
||||||
|
|
||||||
|
for ctx in contexts:
|
||||||
|
tool_name = ctx.metadata.get("tool_name", "unknown")
|
||||||
|
status = ctx.metadata.get("status", "")
|
||||||
|
|
||||||
|
if status:
|
||||||
|
parts.append(f'<tool_result name="{tool_name}" status="{status}">')
|
||||||
|
else:
|
||||||
|
parts.append(f'<tool_result name="{tool_name}">')
|
||||||
|
|
||||||
|
parts.append(ctx.content)
|
||||||
|
parts.append("</tool_result>")
|
||||||
|
|
||||||
|
parts.append("</tool_results>")
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _escape_xml(text: str) -> str:
|
||||||
|
"""Escape XML special characters in attribute values."""
|
||||||
|
return (
|
||||||
|
text.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace("'", "'")
|
||||||
|
)
|
||||||
160
backend/app/services/context/adapters/openai.py
Normal file
160
backend/app/services/context/adapters/openai.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
OpenAI Model Adapter.
|
||||||
|
|
||||||
|
Provides OpenAI-specific context formatting using markdown
|
||||||
|
which GPT models understand well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..types import BaseContext, ContextType
|
||||||
|
from .base import ModelAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAIAdapter(ModelAdapter):
|
||||||
|
"""
|
||||||
|
OpenAI-specific context formatting adapter.
|
||||||
|
|
||||||
|
GPT models work well with markdown formatting,
|
||||||
|
so we use headers and structured markdown for clarity.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Markdown headers for each context type
|
||||||
|
- Bulleted lists for document sources
|
||||||
|
- Bold role labels for conversations
|
||||||
|
- Code blocks for tool outputs
|
||||||
|
"""
|
||||||
|
|
||||||
|
MODEL_PATTERNS: list[str] = ["gpt", "openai", "o1", "o3"]
|
||||||
|
|
||||||
|
def format(
|
||||||
|
self,
|
||||||
|
contexts: list[BaseContext],
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format contexts for OpenAI models.
|
||||||
|
|
||||||
|
Uses markdown formatting for structured content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contexts: List of contexts to format
|
||||||
|
**kwargs: Additional formatting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Markdown-structured context string
|
||||||
|
"""
|
||||||
|
if not contexts:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
by_type = self.group_by_type(contexts)
|
||||||
|
parts: list[str] = []
|
||||||
|
|
||||||
|
for ct in self.get_type_order():
|
||||||
|
if ct in by_type:
|
||||||
|
formatted = self.format_type(by_type[ct], ct, **kwargs)
|
||||||
|
if formatted:
|
||||||
|
parts.append(formatted)
|
||||||
|
|
||||||
|
return self.get_separator().join(parts)
|
||||||
|
|
||||||
|
def format_type(
|
||||||
|
self,
|
||||||
|
contexts: list[BaseContext],
|
||||||
|
context_type: ContextType,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format contexts of a specific type for OpenAI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contexts: List of contexts of the same type
|
||||||
|
context_type: The type of contexts
|
||||||
|
**kwargs: Additional formatting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Markdown-formatted string for this context type
|
||||||
|
"""
|
||||||
|
if not contexts:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if context_type == ContextType.SYSTEM:
|
||||||
|
return self._format_system(contexts)
|
||||||
|
elif context_type == ContextType.TASK:
|
||||||
|
return self._format_task(contexts)
|
||||||
|
elif context_type == ContextType.KNOWLEDGE:
|
||||||
|
return self._format_knowledge(contexts)
|
||||||
|
elif context_type == ContextType.CONVERSATION:
|
||||||
|
return self._format_conversation(contexts)
|
||||||
|
elif context_type == ContextType.TOOL:
|
||||||
|
return self._format_tool(contexts)
|
||||||
|
|
||||||
|
return "\n".join(c.content for c in contexts)
|
||||||
|
|
||||||
|
def _format_system(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""Format system contexts."""
|
||||||
|
content = "\n\n".join(c.content for c in contexts)
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _format_task(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""Format task contexts."""
|
||||||
|
content = "\n\n".join(c.content for c in contexts)
|
||||||
|
return f"## Current Task\n\n{content}"
|
||||||
|
|
||||||
|
def _format_knowledge(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""
|
||||||
|
Format knowledge contexts as structured documents.
|
||||||
|
|
||||||
|
Each knowledge context becomes a section with source attribution.
|
||||||
|
"""
|
||||||
|
parts = ["## Reference Documents\n"]
|
||||||
|
|
||||||
|
for ctx in contexts:
|
||||||
|
source = ctx.source
|
||||||
|
score = ctx.metadata.get("score", ctx.metadata.get("relevance_score", ""))
|
||||||
|
|
||||||
|
if score:
|
||||||
|
parts.append(f"### Source: {source} (relevance: {score})\n")
|
||||||
|
else:
|
||||||
|
parts.append(f"### Source: {source}\n")
|
||||||
|
|
||||||
|
parts.append(ctx.content)
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
def _format_conversation(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""
|
||||||
|
Format conversation contexts as message history.
|
||||||
|
|
||||||
|
Uses bold role labels for clear turn delineation.
|
||||||
|
"""
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
for ctx in contexts:
|
||||||
|
role = ctx.metadata.get("role", "user").upper()
|
||||||
|
parts.append(f"**{role}**: {ctx.content}")
|
||||||
|
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
def _format_tool(self, contexts: list[BaseContext]) -> str:
|
||||||
|
"""
|
||||||
|
Format tool contexts as tool results.
|
||||||
|
|
||||||
|
Each tool result is in a code block with the tool name.
|
||||||
|
"""
|
||||||
|
parts = ["## Recent Tool Results\n"]
|
||||||
|
|
||||||
|
for ctx in contexts:
|
||||||
|
tool_name = ctx.metadata.get("tool_name", "unknown")
|
||||||
|
status = ctx.metadata.get("status", "")
|
||||||
|
|
||||||
|
if status:
|
||||||
|
parts.append(f"### Tool: {tool_name} ({status})\n")
|
||||||
|
else:
|
||||||
|
parts.append(f"### Tool: {tool_name}\n")
|
||||||
|
|
||||||
|
parts.append(f"```\n{ctx.content}\n```")
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
return "\n".join(parts)
|
||||||
521
backend/tests/services/context/test_adapters.py
Normal file
521
backend/tests/services/context/test_adapters.py
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
"""Tests for model adapters."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.services.context.adapters import (
|
||||||
|
ClaudeAdapter,
|
||||||
|
DefaultAdapter,
|
||||||
|
ModelAdapter,
|
||||||
|
OpenAIAdapter,
|
||||||
|
get_adapter,
|
||||||
|
)
|
||||||
|
from app.services.context.types import (
|
||||||
|
ContextType,
|
||||||
|
ConversationContext,
|
||||||
|
KnowledgeContext,
|
||||||
|
MessageRole,
|
||||||
|
SystemContext,
|
||||||
|
TaskContext,
|
||||||
|
ToolContext,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetAdapter:
|
||||||
|
"""Tests for get_adapter function."""
|
||||||
|
|
||||||
|
def test_claude_models(self) -> None:
|
||||||
|
"""Test that Claude models get ClaudeAdapter."""
|
||||||
|
assert isinstance(get_adapter("claude-3-sonnet"), ClaudeAdapter)
|
||||||
|
assert isinstance(get_adapter("claude-3-opus"), ClaudeAdapter)
|
||||||
|
assert isinstance(get_adapter("claude-3-haiku"), ClaudeAdapter)
|
||||||
|
assert isinstance(get_adapter("claude-2"), ClaudeAdapter)
|
||||||
|
assert isinstance(get_adapter("anthropic/claude-3-sonnet"), ClaudeAdapter)
|
||||||
|
|
||||||
|
def test_openai_models(self) -> None:
|
||||||
|
"""Test that OpenAI models get OpenAIAdapter."""
|
||||||
|
assert isinstance(get_adapter("gpt-4"), OpenAIAdapter)
|
||||||
|
assert isinstance(get_adapter("gpt-4-turbo"), OpenAIAdapter)
|
||||||
|
assert isinstance(get_adapter("gpt-3.5-turbo"), OpenAIAdapter)
|
||||||
|
assert isinstance(get_adapter("openai/gpt-4"), OpenAIAdapter)
|
||||||
|
assert isinstance(get_adapter("o1-mini"), OpenAIAdapter)
|
||||||
|
assert isinstance(get_adapter("o3-mini"), OpenAIAdapter)
|
||||||
|
|
||||||
|
def test_unknown_models(self) -> None:
|
||||||
|
"""Test that unknown models get DefaultAdapter."""
|
||||||
|
assert isinstance(get_adapter("llama-2"), DefaultAdapter)
|
||||||
|
assert isinstance(get_adapter("mistral-7b"), DefaultAdapter)
|
||||||
|
assert isinstance(get_adapter("custom-model"), DefaultAdapter)
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelAdapterBase:
|
||||||
|
"""Tests for ModelAdapter base class."""
|
||||||
|
|
||||||
|
def test_get_type_order(self) -> None:
|
||||||
|
"""Test default type ordering."""
|
||||||
|
adapter = DefaultAdapter()
|
||||||
|
order = adapter.get_type_order()
|
||||||
|
|
||||||
|
assert order == [
|
||||||
|
ContextType.SYSTEM,
|
||||||
|
ContextType.TASK,
|
||||||
|
ContextType.KNOWLEDGE,
|
||||||
|
ContextType.CONVERSATION,
|
||||||
|
ContextType.TOOL,
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_group_by_type(self) -> None:
|
||||||
|
"""Test grouping contexts by type."""
|
||||||
|
adapter = DefaultAdapter()
|
||||||
|
|
||||||
|
contexts = [
|
||||||
|
SystemContext(content="System", source="system"),
|
||||||
|
TaskContext(content="Task", source="task"),
|
||||||
|
KnowledgeContext(content="Knowledge", source="docs"),
|
||||||
|
SystemContext(content="System 2", source="system"),
|
||||||
|
]
|
||||||
|
|
||||||
|
grouped = adapter.group_by_type(contexts)
|
||||||
|
|
||||||
|
assert len(grouped[ContextType.SYSTEM]) == 2
|
||||||
|
assert len(grouped[ContextType.TASK]) == 1
|
||||||
|
assert len(grouped[ContextType.KNOWLEDGE]) == 1
|
||||||
|
assert ContextType.CONVERSATION not in grouped
|
||||||
|
|
||||||
|
def test_matches_model_default(self) -> None:
|
||||||
|
"""Test that DefaultAdapter matches all models."""
|
||||||
|
assert DefaultAdapter.matches_model("anything")
|
||||||
|
assert DefaultAdapter.matches_model("claude-3")
|
||||||
|
assert DefaultAdapter.matches_model("gpt-4")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDefaultAdapter:
|
||||||
|
"""Tests for DefaultAdapter."""
|
||||||
|
|
||||||
|
def test_format_empty(self) -> None:
|
||||||
|
"""Test formatting empty context list."""
|
||||||
|
adapter = DefaultAdapter()
|
||||||
|
result = adapter.format([])
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
def test_format_system(self) -> None:
|
||||||
|
"""Test formatting system context."""
|
||||||
|
adapter = DefaultAdapter()
|
||||||
|
contexts = [
|
||||||
|
SystemContext(content="You are helpful.", source="system"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "You are helpful." in result
|
||||||
|
|
||||||
|
def test_format_task(self) -> None:
|
||||||
|
"""Test formatting task context."""
|
||||||
|
adapter = DefaultAdapter()
|
||||||
|
contexts = [
|
||||||
|
TaskContext(content="Write a function.", source="task"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "Task:" in result
|
||||||
|
assert "Write a function." in result
|
||||||
|
|
||||||
|
def test_format_knowledge(self) -> None:
|
||||||
|
"""Test formatting knowledge context."""
|
||||||
|
adapter = DefaultAdapter()
|
||||||
|
contexts = [
|
||||||
|
KnowledgeContext(content="Documentation here.", source="docs"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "Reference Information:" in result
|
||||||
|
assert "Documentation here." in result
|
||||||
|
|
||||||
|
def test_format_conversation(self) -> None:
|
||||||
|
"""Test formatting conversation context."""
|
||||||
|
adapter = DefaultAdapter()
|
||||||
|
contexts = [
|
||||||
|
ConversationContext(
|
||||||
|
content="Hello!",
|
||||||
|
source="chat",
|
||||||
|
role=MessageRole.USER,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "Previous Conversation:" in result
|
||||||
|
assert "Hello!" in result
|
||||||
|
|
||||||
|
def test_format_tool(self) -> None:
|
||||||
|
"""Test formatting tool context."""
|
||||||
|
adapter = DefaultAdapter()
|
||||||
|
contexts = [
|
||||||
|
ToolContext(
|
||||||
|
content="Result: success",
|
||||||
|
source="tool",
|
||||||
|
metadata={"tool_name": "search"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "Tool Results:" in result
|
||||||
|
assert "Result: success" in result
|
||||||
|
|
||||||
|
|
||||||
|
class TestClaudeAdapter:
|
||||||
|
"""Tests for ClaudeAdapter."""
|
||||||
|
|
||||||
|
def test_matches_model(self) -> None:
|
||||||
|
"""Test model matching."""
|
||||||
|
assert ClaudeAdapter.matches_model("claude-3-sonnet")
|
||||||
|
assert ClaudeAdapter.matches_model("claude-3-opus")
|
||||||
|
assert ClaudeAdapter.matches_model("anthropic/claude-3-haiku")
|
||||||
|
assert not ClaudeAdapter.matches_model("gpt-4")
|
||||||
|
assert not ClaudeAdapter.matches_model("llama-2")
|
||||||
|
|
||||||
|
def test_format_empty(self) -> None:
|
||||||
|
"""Test formatting empty context list."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
result = adapter.format([])
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
def test_format_system_uses_xml(self) -> None:
|
||||||
|
"""Test that system context uses XML tags."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
contexts = [
|
||||||
|
SystemContext(content="You are helpful.", source="system"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "<system_instructions>" in result
|
||||||
|
assert "</system_instructions>" in result
|
||||||
|
assert "You are helpful." in result
|
||||||
|
|
||||||
|
def test_format_task_uses_xml(self) -> None:
|
||||||
|
"""Test that task context uses XML tags."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
contexts = [
|
||||||
|
TaskContext(content="Write a function.", source="task"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "<current_task>" in result
|
||||||
|
assert "</current_task>" in result
|
||||||
|
assert "Write a function." in result
|
||||||
|
|
||||||
|
def test_format_knowledge_uses_document_tags(self) -> None:
|
||||||
|
"""Test that knowledge uses document XML tags."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
contexts = [
|
||||||
|
KnowledgeContext(
|
||||||
|
content="Documentation here.",
|
||||||
|
source="docs/api.md",
|
||||||
|
relevance_score=0.9,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "<reference_documents>" in result
|
||||||
|
assert "</reference_documents>" in result
|
||||||
|
assert '<document source="docs/api.md"' in result
|
||||||
|
assert "</document>" in result
|
||||||
|
assert "Documentation here." in result
|
||||||
|
|
||||||
|
def test_format_knowledge_with_score(self) -> None:
|
||||||
|
"""Test that knowledge includes relevance score."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
contexts = [
|
||||||
|
KnowledgeContext(
|
||||||
|
content="Doc content.",
|
||||||
|
source="docs/api.md",
|
||||||
|
metadata={"relevance_score": 0.95},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert 'relevance="0.95"' in result
|
||||||
|
|
||||||
|
def test_format_conversation_uses_message_tags(self) -> None:
|
||||||
|
"""Test that conversation uses message XML tags."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
contexts = [
|
||||||
|
ConversationContext(
|
||||||
|
content="Hello!",
|
||||||
|
source="chat",
|
||||||
|
role=MessageRole.USER,
|
||||||
|
metadata={"role": "user"},
|
||||||
|
),
|
||||||
|
ConversationContext(
|
||||||
|
content="Hi there!",
|
||||||
|
source="chat",
|
||||||
|
role=MessageRole.ASSISTANT,
|
||||||
|
metadata={"role": "assistant"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "<conversation_history>" in result
|
||||||
|
assert "</conversation_history>" in result
|
||||||
|
assert '<message role="user">' in result
|
||||||
|
assert '<message role="assistant">' in result
|
||||||
|
assert "Hello!" in result
|
||||||
|
assert "Hi there!" in result
|
||||||
|
|
||||||
|
def test_format_tool_uses_tool_result_tags(self) -> None:
|
||||||
|
"""Test that tool results use tool_result XML tags."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
contexts = [
|
||||||
|
ToolContext(
|
||||||
|
content='{"status": "ok"}',
|
||||||
|
source="tool",
|
||||||
|
metadata={"tool_name": "search", "status": "success"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "<tool_results>" in result
|
||||||
|
assert "</tool_results>" in result
|
||||||
|
assert '<tool_result name="search"' in result
|
||||||
|
assert 'status="success"' in result
|
||||||
|
assert "</tool_result>" in result
|
||||||
|
|
||||||
|
def test_format_multiple_types_in_order(self) -> None:
|
||||||
|
"""Test that multiple types are formatted in correct order."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
contexts = [
|
||||||
|
KnowledgeContext(content="Knowledge", source="docs"),
|
||||||
|
SystemContext(content="System", source="system"),
|
||||||
|
TaskContext(content="Task", source="task"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
|
||||||
|
# Find positions
|
||||||
|
system_pos = result.find("<system_instructions>")
|
||||||
|
task_pos = result.find("<current_task>")
|
||||||
|
knowledge_pos = result.find("<reference_documents>")
|
||||||
|
|
||||||
|
# Verify order
|
||||||
|
assert system_pos < task_pos < knowledge_pos
|
||||||
|
|
||||||
|
def test_escape_xml_in_source(self) -> None:
|
||||||
|
"""Test that XML special chars are escaped in source."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
contexts = [
|
||||||
|
KnowledgeContext(
|
||||||
|
content="Doc content.",
|
||||||
|
source='path/with"quotes&stuff.md',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert """ in result
|
||||||
|
assert "&" in result
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpenAIAdapter:
|
||||||
|
"""Tests for OpenAIAdapter."""
|
||||||
|
|
||||||
|
def test_matches_model(self) -> None:
|
||||||
|
"""Test model matching."""
|
||||||
|
assert OpenAIAdapter.matches_model("gpt-4")
|
||||||
|
assert OpenAIAdapter.matches_model("gpt-4-turbo")
|
||||||
|
assert OpenAIAdapter.matches_model("gpt-3.5-turbo")
|
||||||
|
assert OpenAIAdapter.matches_model("openai/gpt-4")
|
||||||
|
assert OpenAIAdapter.matches_model("o1-preview")
|
||||||
|
assert OpenAIAdapter.matches_model("o3-mini")
|
||||||
|
assert not OpenAIAdapter.matches_model("claude-3")
|
||||||
|
assert not OpenAIAdapter.matches_model("llama-2")
|
||||||
|
|
||||||
|
def test_format_empty(self) -> None:
|
||||||
|
"""Test formatting empty context list."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
result = adapter.format([])
|
||||||
|
assert result == ""
|
||||||
|
|
||||||
|
def test_format_system_plain(self) -> None:
|
||||||
|
"""Test that system content is plain."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
contexts = [
|
||||||
|
SystemContext(content="You are helpful.", source="system"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
# System content should be plain without headers
|
||||||
|
assert "You are helpful." in result
|
||||||
|
assert "##" not in result # No markdown headers for system
|
||||||
|
|
||||||
|
def test_format_task_uses_markdown(self) -> None:
|
||||||
|
"""Test that task uses markdown headers."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
contexts = [
|
||||||
|
TaskContext(content="Write a function.", source="task"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "## Current Task" in result
|
||||||
|
assert "Write a function." in result
|
||||||
|
|
||||||
|
def test_format_knowledge_uses_markdown(self) -> None:
|
||||||
|
"""Test that knowledge uses markdown with source headers."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
contexts = [
|
||||||
|
KnowledgeContext(
|
||||||
|
content="Documentation here.",
|
||||||
|
source="docs/api.md",
|
||||||
|
relevance_score=0.9,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "## Reference Documents" in result
|
||||||
|
assert "### Source: docs/api.md" in result
|
||||||
|
assert "Documentation here." in result
|
||||||
|
|
||||||
|
def test_format_knowledge_with_score(self) -> None:
|
||||||
|
"""Test that knowledge includes relevance score."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
contexts = [
|
||||||
|
KnowledgeContext(
|
||||||
|
content="Doc content.",
|
||||||
|
source="docs/api.md",
|
||||||
|
metadata={"relevance_score": 0.95},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "(relevance: 0.95)" in result
|
||||||
|
|
||||||
|
def test_format_conversation_uses_bold_roles(self) -> None:
|
||||||
|
"""Test that conversation uses bold role labels."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
contexts = [
|
||||||
|
ConversationContext(
|
||||||
|
content="Hello!",
|
||||||
|
source="chat",
|
||||||
|
role=MessageRole.USER,
|
||||||
|
metadata={"role": "user"},
|
||||||
|
),
|
||||||
|
ConversationContext(
|
||||||
|
content="Hi there!",
|
||||||
|
source="chat",
|
||||||
|
role=MessageRole.ASSISTANT,
|
||||||
|
metadata={"role": "assistant"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "**USER**:" in result
|
||||||
|
assert "**ASSISTANT**:" in result
|
||||||
|
assert "Hello!" in result
|
||||||
|
assert "Hi there!" in result
|
||||||
|
|
||||||
|
def test_format_tool_uses_code_blocks(self) -> None:
|
||||||
|
"""Test that tool results use code blocks."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
contexts = [
|
||||||
|
ToolContext(
|
||||||
|
content='{"status": "ok"}',
|
||||||
|
source="tool",
|
||||||
|
metadata={"tool_name": "search", "status": "success"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
assert "## Recent Tool Results" in result
|
||||||
|
assert "### Tool: search (success)" in result
|
||||||
|
assert "```" in result # Code block
|
||||||
|
assert '{"status": "ok"}' in result
|
||||||
|
|
||||||
|
def test_format_multiple_types_in_order(self) -> None:
|
||||||
|
"""Test that multiple types are formatted in correct order."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
contexts = [
|
||||||
|
KnowledgeContext(content="Knowledge", source="docs"),
|
||||||
|
SystemContext(content="System", source="system"),
|
||||||
|
TaskContext(content="Task", source="task"),
|
||||||
|
]
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
|
||||||
|
# System comes first (no header), then task, then knowledge
|
||||||
|
system_pos = result.find("System")
|
||||||
|
task_pos = result.find("## Current Task")
|
||||||
|
knowledge_pos = result.find("## Reference Documents")
|
||||||
|
|
||||||
|
assert system_pos < task_pos < knowledge_pos
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdapterIntegration:
|
||||||
|
"""Integration tests for adapters."""
|
||||||
|
|
||||||
|
def test_full_context_formatting_claude(self) -> None:
|
||||||
|
"""Test formatting a full set of contexts for Claude."""
|
||||||
|
adapter = ClaudeAdapter()
|
||||||
|
|
||||||
|
contexts = [
|
||||||
|
SystemContext(
|
||||||
|
content="You are an expert Python developer.",
|
||||||
|
source="system",
|
||||||
|
),
|
||||||
|
TaskContext(
|
||||||
|
content="Implement user authentication.",
|
||||||
|
source="task:AUTH-123",
|
||||||
|
),
|
||||||
|
KnowledgeContext(
|
||||||
|
content="JWT tokens provide stateless authentication...",
|
||||||
|
source="docs/auth/jwt.md",
|
||||||
|
relevance_score=0.9,
|
||||||
|
),
|
||||||
|
ConversationContext(
|
||||||
|
content="Can you help me implement JWT auth?",
|
||||||
|
source="chat",
|
||||||
|
role=MessageRole.USER,
|
||||||
|
metadata={"role": "user"},
|
||||||
|
),
|
||||||
|
ToolContext(
|
||||||
|
content='{"file": "auth.py", "status": "created"}',
|
||||||
|
source="tool",
|
||||||
|
metadata={"tool_name": "file_create"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
|
||||||
|
# Verify all sections present
|
||||||
|
assert "<system_instructions>" in result
|
||||||
|
assert "<current_task>" in result
|
||||||
|
assert "<reference_documents>" in result
|
||||||
|
assert "<conversation_history>" in result
|
||||||
|
assert "<tool_results>" in result
|
||||||
|
|
||||||
|
# Verify content
|
||||||
|
assert "expert Python developer" in result
|
||||||
|
assert "user authentication" in result
|
||||||
|
assert "JWT tokens" in result
|
||||||
|
assert "help me implement" in result
|
||||||
|
assert "file_create" in result
|
||||||
|
|
||||||
|
def test_full_context_formatting_openai(self) -> None:
|
||||||
|
"""Test formatting a full set of contexts for OpenAI."""
|
||||||
|
adapter = OpenAIAdapter()
|
||||||
|
|
||||||
|
contexts = [
|
||||||
|
SystemContext(
|
||||||
|
content="You are an expert Python developer.",
|
||||||
|
source="system",
|
||||||
|
),
|
||||||
|
TaskContext(
|
||||||
|
content="Implement user authentication.",
|
||||||
|
source="task:AUTH-123",
|
||||||
|
),
|
||||||
|
KnowledgeContext(
|
||||||
|
content="JWT tokens provide stateless authentication...",
|
||||||
|
source="docs/auth/jwt.md",
|
||||||
|
relevance_score=0.9,
|
||||||
|
),
|
||||||
|
ConversationContext(
|
||||||
|
content="Can you help me implement JWT auth?",
|
||||||
|
source="chat",
|
||||||
|
role=MessageRole.USER,
|
||||||
|
metadata={"role": "user"},
|
||||||
|
),
|
||||||
|
ToolContext(
|
||||||
|
content='{"file": "auth.py", "status": "created"}',
|
||||||
|
source="tool",
|
||||||
|
metadata={"tool_name": "file_create"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = adapter.format(contexts)
|
||||||
|
|
||||||
|
# Verify all sections present
|
||||||
|
assert "## Current Task" in result
|
||||||
|
assert "## Reference Documents" in result
|
||||||
|
assert "## Recent Tool Results" in result
|
||||||
|
assert "**USER**:" in result
|
||||||
|
|
||||||
|
# Verify content
|
||||||
|
assert "expert Python developer" in result
|
||||||
|
assert "user authentication" in result
|
||||||
|
assert "JWT tokens" in result
|
||||||
|
assert "help me implement" in result
|
||||||
|
assert "file_create" in result
|
||||||
Reference in New Issue
Block a user