diff --git a/backend/app/services/context/__init__.py b/backend/app/services/context/__init__.py
index 5ad719c..107c27e 100644
--- a/backend/app/services/context/__init__.py
+++ b/backend/app/services/context/__init__.py
@@ -76,6 +76,15 @@ from .compression import (
TruncationStrategy,
)
+# Adapters
+from .adapters import (
+ ClaudeAdapter,
+ DefaultAdapter,
+ get_adapter,
+ ModelAdapter,
+ OpenAIAdapter,
+)
+
# Prioritization
from .prioritization import (
ContextRanker,
@@ -110,6 +119,12 @@ from .types import (
)
__all__ = [
+ # Adapters
+ "ClaudeAdapter",
+ "DefaultAdapter",
+ "get_adapter",
+ "ModelAdapter",
+ "OpenAIAdapter",
# Assembly
"ContextPipeline",
"PipelineMetrics",
diff --git a/backend/app/services/context/adapters/__init__.py b/backend/app/services/context/adapters/__init__.py
index fbf30a4..0194a04 100644
--- a/backend/app/services/context/adapters/__init__.py
+++ b/backend/app/services/context/adapters/__init__.py
@@ -1,5 +1,35 @@
"""
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",
+]
diff --git a/backend/app/services/context/adapters/base.py b/backend/app/services/context/adapters/base.py
new file mode 100644
index 0000000..cd0d6a0
--- /dev/null
+++ b/backend/app/services/context/adapters/base.py
@@ -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
diff --git a/backend/app/services/context/adapters/claude.py b/backend/app/services/context/adapters/claude.py
new file mode 100644
index 0000000..0c0e253
--- /dev/null
+++ b/backend/app/services/context/adapters/claude.py
@@ -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"\n{content}\n"
+
+ def _format_task(self, contexts: list[BaseContext]) -> str:
+ """Format task contexts."""
+ content = "\n\n".join(c.content for c in contexts)
+ return f"\n{content}\n"
+
+ def _format_knowledge(self, contexts: list[BaseContext]) -> str:
+ """
+ Format knowledge contexts as structured documents.
+
+ Each knowledge context becomes a document with source attribution.
+ """
+ parts = [""]
+
+ 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'')
+ else:
+ parts.append(f'')
+
+ parts.append(content)
+ parts.append("")
+
+ parts.append("")
+ 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 = [""]
+
+ for ctx in contexts:
+ role = ctx.metadata.get("role", "user")
+ parts.append(f'')
+ parts.append(ctx.content)
+ parts.append("")
+
+ parts.append("")
+ 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 = [""]
+
+ for ctx in contexts:
+ tool_name = ctx.metadata.get("tool_name", "unknown")
+ status = ctx.metadata.get("status", "")
+
+ if status:
+ parts.append(f'')
+ else:
+ parts.append(f'')
+
+ parts.append(ctx.content)
+ parts.append("")
+
+ parts.append("")
+ 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("'", "'")
+ )
diff --git a/backend/app/services/context/adapters/openai.py b/backend/app/services/context/adapters/openai.py
new file mode 100644
index 0000000..40304b7
--- /dev/null
+++ b/backend/app/services/context/adapters/openai.py
@@ -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)
diff --git a/backend/tests/services/context/test_adapters.py b/backend/tests/services/context/test_adapters.py
new file mode 100644
index 0000000..9013d7f
--- /dev/null
+++ b/backend/tests/services/context/test_adapters.py
@@ -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 "" in result
+ assert "" 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 "" in result
+ assert "" 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 "" in result
+ assert "" in result
+ assert '" 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 "" in result
+ assert "" in result
+ assert '' in result
+ assert '' 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 "" in result
+ assert "" in result
+ assert '" 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("")
+ task_pos = result.find("")
+ knowledge_pos = result.find("")
+
+ # 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 "" in result
+ assert "" in result
+ assert "" in result
+ assert "" in result
+ assert "" 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