""" Claude Model Adapter. Provides Claude-specific context formatting using XML tags which Claude models understand natively. """ from typing import Any, ClassVar 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: ClassVar[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) # Fallback for any unhandled context types - still escape content # to prevent XML injection if new types are added without updating adapter return "\n".join(self._escape_xml_content(c.content) for c in contexts) def _format_system(self, contexts: list[BaseContext]) -> str: """Format system contexts.""" # System prompts are typically admin-controlled, but escape for safety content = "\n\n".join(self._escape_xml_content(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(self._escape_xml_content(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. All content is XML-escaped to prevent injection attacks. """ parts = [""] for ctx in contexts: source = self._escape_xml(ctx.source) # Escape content to prevent XML injection content = self._escape_xml_content(ctx.content) score = ctx.metadata.get("score", ctx.metadata.get("relevance_score", "")) if score: # Escape score to prevent XML injection via metadata escaped_score = self._escape_xml(str(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. All content is XML-escaped to prevent prompt injection. """ parts = [""] for ctx in contexts: role = self._escape_xml(ctx.metadata.get("role", "user")) # Escape content to prevent prompt injection via fake XML tags content = self._escape_xml_content(ctx.content) parts.append(f'') parts.append(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. All content is XML-escaped to prevent injection. """ parts = [""] for ctx in contexts: tool_name = self._escape_xml(ctx.metadata.get("tool_name", "unknown")) status = ctx.metadata.get("status", "") if status: parts.append( f'' ) else: parts.append(f'') # Escape content to prevent injection parts.append(self._escape_xml_content(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("'", "'") ) @staticmethod def _escape_xml_content(text: str) -> str: """ Escape XML special characters in element content. This prevents XML injection attacks where malicious content could break out of XML tags or inject fake tags for prompt injection. Only escapes &, <, > since quotes don't need escaping in content. Args: text: Content text to escape Returns: XML-safe content string """ return text.replace("&", "&").replace("<", "<").replace(">", ">")