diff --git a/agentpress/examples/example_agent/agent.py b/agentpress/examples/example_agent/agent.py index 180772f2..5a41deca 100644 --- a/agentpress/examples/example_agent/agent.py +++ b/agentpress/examples/example_agent/agent.py @@ -78,7 +78,9 @@ Use XML tags to specify file operations: file contents here - + +text to replace +replacement text @@ -137,7 +139,7 @@ Current development environment workspace state: """ } - model_name = "anthropic/claude-3-5-haiku-latest" + model_name = "anthropic/claude-3-5-sonnet-latest" registry = thread_manager.tool_registry tool_parser = XMLToolParser(tool_registry=registry) diff --git a/agentpress/examples/example_agent/tools/files_tool.py b/agentpress/examples/example_agent/tools/files_tool.py index 9b4568b7..485d52f0 100644 --- a/agentpress/examples/example_agent/tools/files_tool.py +++ b/agentpress/examples/example_agent/tools/files_tool.py @@ -134,8 +134,11 @@ class FilesTool(Tool): }) @xml_schema( tag_name="create-file", - attributes={"file_path": "file_path"}, - param_mapping={".": "file_contents"} + mappings=[ + {"param_name": "file_path", "node_type": "attribute", "path": "."}, + {"param_name": "file_contents", "node_type": "content", "path": "."} + ], + description="Create a new file with the provided contents" ) async def create_file(self, file_path: str, file_contents: str) -> ToolResult: try: @@ -171,7 +174,10 @@ class FilesTool(Tool): }) @xml_schema( tag_name="delete-file", - attributes={"file_path": "file_path"} + mappings=[ + {"param_name": "file_path", "node_type": "attribute", "path": "."} + ], + description="Delete a file at the given path" ) async def delete_file(self, file_path: str) -> ToolResult: try: @@ -210,11 +216,12 @@ class FilesTool(Tool): }) @xml_schema( tag_name="str-replace", - attributes={"file_path": "file_path"}, - param_mapping={ - "old_str": "old_str", - "new_str": "new_str" - } + mappings=[ + {"param_name": "file_path", "node_type": "attribute", "path": "file_path"}, + {"param_name": "old_str", "node_type": "element", "path": "old_str"}, + {"param_name": "new_str", "node_type": "element", "path": "new_str"} + ], + description="Replace text in a file" ) async def str_replace(self, file_path: str, old_str: str, new_str: str) -> ToolResult: try: diff --git a/agentpress/examples/example_agent/tools/terminal_tool.py b/agentpress/examples/example_agent/tools/terminal_tool.py index d858a6bd..f5ad66a6 100644 --- a/agentpress/examples/example_agent/tools/terminal_tool.py +++ b/agentpress/examples/example_agent/tools/terminal_tool.py @@ -43,8 +43,10 @@ class TerminalTool(Tool): }) @xml_schema( tag_name="execute-command", - attributes={}, - param_mapping={".": "command"} + mappings=[ + {"param_name": "command", "node_type": "content", "path": "."} + ], + description="Execute a shell command in the workspace directory" ) async def execute_command(self, command: str) -> ToolResult: original_dir = os.getcwd() diff --git a/agentpress/examples/example_agent/workspace/index.html b/agentpress/examples/example_agent/workspace/index.html deleted file mode 100644 index 7b8e7fba..00000000 --- a/agentpress/examples/example_agent/workspace/index.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - Modern Landing Page - - - - - - - - - -
- -
- -
-
-
-

Transform Your Business

-

Innovative solutions for modern challenges

- Get Started -
-
- -
-
-
- - - - -

Smart Design

-

Elegant and intuitive user interfaces

-
-
- - - -

Performance

-

High-speed, optimized solutions

-
-
- - - -

Scalability

-

Grow without limitations

-
-
-
- -
-
-

About Our Company

-
-
-

We are a passionate team dedicated to delivering cutting-edge solutions that transform businesses. Our mission is to empower organizations through innovative technology and strategic design.

-

With years of experience and a commitment to excellence, we help our clients achieve their goals and stay ahead in a rapidly evolving digital landscape.

-
-
- Team Working -
-
-
-
- -
-
-

Contact Us

-
- - - - -
-
-
-
- - - - - - \ No newline at end of file diff --git a/agentpress/examples/example_agent/workspace/script.js b/agentpress/examples/example_agent/workspace/script.js deleted file mode 100644 index ec521fd6..00000000 --- a/agentpress/examples/example_agent/workspace/script.js +++ /dev/null @@ -1,54 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const mobileMenu = document.querySelector('.mobile-menu'); - const navLinks = document.querySelector('.nav-links'); - const contactForm = document.getElementById('contact-form'); - - mobileMenu.addEventListener('click', () => { - navLinks.classList.toggle('active'); - mobileMenu.classList.toggle('open'); - }); - - const smoothScroll = (target) => { - const element = document.querySelector(target); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - } - }; - - document.querySelectorAll('a[href^="#"]').forEach(anchor => { - anchor.addEventListener('click', function(e) { - e.preventDefault(); - smoothScroll(this.getAttribute('href')); - - if (navLinks.classList.contains('active')) { - navLinks.classList.remove('active'); - mobileMenu.classList.remove('open'); - } - }); - }); - - contactForm.addEventListener('submit', (e) => { - e.preventDefault(); - const formData = new FormData(contactForm); - const data = Object.fromEntries(formData.entries()); - - alert(`Thank you for your message, ${data.name}! We'll get back to you soon.`); - contactForm.reset(); - }); - - const observerOptions = { - threshold: 0.1 - }; - - const fadeInObserver = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - entry.target.classList.add('fade-in'); - } - }); - }, observerOptions); - - document.querySelectorAll('.feature, .about-content, .contact-container').forEach(section => { - fadeInObserver.observe(section); - }); -}); \ No newline at end of file diff --git a/agentpress/examples/example_agent/workspace/styles.css b/agentpress/examples/example_agent/workspace/styles.css deleted file mode 100644 index 1ccede6a..00000000 --- a/agentpress/examples/example_agent/workspace/styles.css +++ /dev/null @@ -1,295 +0,0 @@ -:root { - --primary-color: #3498db; - --secondary-color: #2ecc71; - --text-color: #333; - --background-color: #f4f4f4; -} - -*, -*::before, -*::after { - margin: 0; - padding: 0; - box-sizing: border-box; - scroll-behavior: smooth; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -.fade-in { - animation: fadeIn 0.8s ease-out forwards; - opacity: 0; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - line-height: 1.6; - color: var(--text-color); - background-color: var(--background-color); -} - -header { - position: fixed; - width: 100%; - top: 0; - left: 0; - background-color: white; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - z-index: 1000; -} - -nav { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 5%; - max-width: 1200px; - margin: 0 auto; -} - -.logo { - font-size: 1.5rem; - font-weight: bold; - color: var(--primary-color); -} - -.nav-links { - display: flex; - align-items: center; - gap: 1.5rem; -} - -.nav-links a { - text-decoration: none; - color: var(--text-color); - transition: color 0.3s ease; -} - -.nav-links a:hover { - color: var(--primary-color); -} - -.cta-button { - background-color: var(--primary-color); - color: white; - padding: 0.5rem 1rem; - border-radius: 4px; -} - -.mobile-menu { - display: none; - flex-direction: column; - cursor: pointer; -} - -.mobile-menu span { - width: 25px; - height: 3px; - background-color: var(--text-color); - margin: 2px 0; - transition: 0.4s; -} - -.hero { - height: 100vh; - display: flex; - align-items: center; - background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); - color: white; - text-align: center; -} - -.hero-content { - max-width: 800px; - margin: 0 auto; - padding: 0 5%; -} - -.hero h1 { - font-size: 3rem; - margin-bottom: 1rem; -} - -.hero p { - font-size: 1.2rem; - margin-bottom: 2rem; -} - -.primary-button { - display: inline-block; - background-color: white; - color: var(--primary-color); - padding: 0.75rem 1.5rem; - text-decoration: none; - border-radius: 4px; - font-weight: bold; - transition: background-color 0.3s ease; -} - -.primary-button:hover { - background-color: #f4f4f4; -} - -.features { - padding: 4rem 5%; - background-color: white; -} - -.feature-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 2rem; - max-width: 1200px; - margin: 0 auto; -} - -.feature { - text-align: center; - padding: 2rem; - background-color: var(--background-color); - border-radius: 8px; - transition: transform 0.3s ease; -} - -.feature:hover { - transform: translateY(-10px); -} - -.feature-icon { - width: 64px; - height: 64px; - fill: var(--primary-color); - margin-bottom: 1rem; -} - -footer { - background-color: var(--text-color); - color: white; - padding: 2rem; -} - -.footer-content { - max-width: 1200px; - margin: 0 auto; - display: flex; - flex-direction: column; - align-items: center; -} - -.footer-links { - display: flex; - gap: 1.5rem; - margin-bottom: 1rem; -} - -.footer-links a { - color: white; - text-decoration: none; - transition: color 0.3s ease; -} - -.footer-links a:hover { - color: var(--primary-color); -} - -.about { - padding: 4rem 5%; - background-color: white; -} - -.about-content { - max-width: 1200px; - margin: 0 auto; -} - -.about-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - align-items: center; -} - -.about-text p { - margin-bottom: 1rem; - line-height: 1.6; -} - -.about-image img { - width: 100%; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); -} - -.contact { - background-color: var(--background-color); - padding: 4rem 5%; -} - -.contact-container { - max-width: 600px; - margin: 0 auto; - text-align: center; -} - -.contact-form { - display: flex; - flex-direction: column; - gap: 1rem; - background-color: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); -} - -.contact-form input, -.contact-form textarea { - padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; -} - -.contact-form textarea { - min-height: 150px; - resize: vertical; -} - -.submit-button { - background-color: var(--primary-color); - color: white; - border: none; - padding: 0.75rem; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.3s ease; -} - -.submit-button:hover { - background-color: #2980b9; -} - -@media screen and (max-width: 768px) { - .nav-links { - display: none; - } - - .mobile-menu { - display: flex; - } - - .feature-grid, - .about-grid { - grid-template-columns: 1fr; - } - - .hero h1 { - font-size: 2rem; - } - - .about-image { - margin-top: 1rem; - } -} \ No newline at end of file diff --git a/agentpress/tool.py b/agentpress/tool.py index b4babbd8..bde9ac52 100644 --- a/agentpress/tool.py +++ b/agentpress/tool.py @@ -14,12 +14,27 @@ class SchemaType(Enum): XML = "xml" CUSTOM = "custom" +@dataclass +class XMLNodeMapping: + """Maps an XML node (element or attribute) to a function parameter""" + param_name: str # Name of the function parameter + node_type: str = "element" # "element", "attribute", or "content" + path: str = "." # XPath-like path to the node, "." means root element + @dataclass class XMLTagSchema: - """Schema for XML tool tags""" - tag_name: str # e.g. "str-replace" - param_mapping: Dict[str, str] = field(default_factory=dict) # Maps XML elements to function params - attributes: Dict[str, str] = field(default_factory=dict) # Maps XML attributes to function params + """Schema for XML tool tags with improved node mapping""" + tag_name: str # Root tag name (e.g. "str-replace") + mappings: List[XMLNodeMapping] = field(default_factory=list) + description: Optional[str] = None + + def add_mapping(self, param_name: str, node_type: str = "element", path: str = ".") -> None: + """Add a new node mapping""" + self.mappings.append(XMLNodeMapping( + param_name=param_name, + node_type=node_type, + path=path + )) @dataclass class ToolSchema: @@ -81,37 +96,47 @@ def openapi_schema(schema: Dict[str, Any]): def xml_schema( tag_name: str, - param_mapping: Dict[str, str] = None, - attributes: Dict[str, str] = None + mappings: List[Dict[str, str]] = None, + description: str = None ): """ - Decorator for XML schema tools with flexible content mapping. + Decorator for XML schema tools with improved node mapping. Args: tag_name: Name of the root XML tag - param_mapping: Maps XML elements (content or nested tags) to function parameters - attributes: Maps XML attributes to function parameters + mappings: List of mapping definitions, each containing: + - param_name: Name of the function parameter + - node_type: "element", "attribute", or "content" + - path: Path to the node (default "." for root) + description: Optional description of the tool Example: @xml_schema( tag_name="str-replace", - attributes={"file_path": "file_path"}, - param_mapping={ - ".": "new_str", # "." means root tag content - "old_str": "old_str", # nested tag name -> param name - "new_str": "new_str" - } + mappings=[ + {"param_name": "file_path", "node_type": "attribute", "path": "."}, + {"param_name": "old_str", "node_type": "element", "path": "old_str"}, + {"param_name": "new_str", "node_type": "element", "path": "new_str"} + ], + description="Replace text in a file" ) """ def decorator(func): + xml_schema = XMLTagSchema(tag_name=tag_name, description=description) + + # Add mappings + if mappings: + for mapping in mappings: + xml_schema.add_mapping( + param_name=mapping["param_name"], + node_type=mapping.get("node_type", "element"), + path=mapping.get("path", ".") + ) + return _add_schema(func, ToolSchema( schema_type=SchemaType.XML, - schema={}, - xml_schema=XMLTagSchema( - tag_name=tag_name, - param_mapping=param_mapping or {}, - attributes=attributes or {} - ) + schema={}, # OpenAPI schema could be added here if needed + xml_schema=xml_schema )) return decorator diff --git a/agentpress/xml_tool_parser.py b/agentpress/xml_tool_parser.py index 710a5563..20dcd22e 100644 --- a/agentpress/xml_tool_parser.py +++ b/agentpress/xml_tool_parser.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, Any, Optional, List +from typing import Dict, Any, Optional, List, Tuple from agentpress.thread_llm_response_processor import ToolParserBase import json import re @@ -9,6 +9,239 @@ class XMLToolParser(ToolParserBase): def __init__(self, tool_registry: Optional[ToolRegistry] = None): self.tool_registry = tool_registry or ToolRegistry() + def _extract_tag_content(self, xml_chunk: str, tag_name: str) -> Tuple[Optional[str], Optional[str]]: + """ + Extract content between opening and closing tags, handling nested tags. + + Args: + xml_chunk: The XML content to parse + tag_name: Name of the tag to find + + Returns: + Tuple of (content, remaining_chunk) + """ + start_tag = f'<{tag_name}' + end_tag = f'' + + try: + # Find start tag position + start_pos = xml_chunk.find(start_tag) + if start_pos == -1: + return None, xml_chunk + + # Find end of opening tag + tag_end = xml_chunk.find('>', start_pos) + if tag_end == -1: + return None, xml_chunk + + # Find matching closing tag + content_start = tag_end + 1 + nesting_level = 1 + pos = content_start + + while nesting_level > 0 and pos < len(xml_chunk): + next_start = xml_chunk.find(start_tag, pos) + next_end = xml_chunk.find(end_tag, pos) + + if next_end == -1: + return None, xml_chunk + + if next_start != -1 and next_start < next_end: + nesting_level += 1 + pos = next_start + len(start_tag) + else: + nesting_level -= 1 + pos = next_end + len(end_tag) + + if nesting_level == 0: + content = xml_chunk[content_start:pos - len(end_tag)] + remaining = xml_chunk[pos:] + return content, remaining + + return None, xml_chunk + + except Exception as e: + logging.error(f"Error extracting tag content: {e}") + return None, xml_chunk + + def _extract_attribute(self, opening_tag: str, attr_name: str) -> Optional[str]: + """ + Extract attribute value from opening tag, handling different quote types and escaping. + + Args: + opening_tag: The opening XML tag + attr_name: Name of the attribute to find + + Returns: + Attribute value if found, None otherwise + """ + try: + # Handle both single and double quotes with raw strings + patterns = [ + fr'{attr_name}="([^"]*)"', # Double quotes + fr"{attr_name}='([^']*)'", # Single quotes + fr'{attr_name}=([^\s/>;]+)' # No quotes - fixed escape sequence + ] + + for pattern in patterns: + match = re.search(pattern, opening_tag) + if match: + value = match.group(1) + # Unescape common XML entities + value = value.replace('"', '"').replace(''', "'") + value = value.replace('<', '<').replace('>', '>') + value = value.replace('&', '&') + return value + + return None + + except Exception as e: + logging.error(f"Error extracting attribute: {e}") + return None + + def _extract_xml_chunks(self, content: str) -> List[str]: + """Extract complete XML chunks using start and end pattern matching.""" + chunks = [] + pos = 0 + + try: + while pos < len(content): + # Find the next tool tag + next_tag_start = -1 + current_tag = None + + # Find the earliest occurrence of any registered tag + for tag_name in self.tool_registry.xml_tools.keys(): + start_pattern = f'<{tag_name}' + tag_pos = content.find(start_pattern, pos) + + if tag_pos != -1 and (next_tag_start == -1 or tag_pos < next_tag_start): + next_tag_start = tag_pos + current_tag = tag_name + + if next_tag_start == -1 or not current_tag: + break + + # Find the matching end tag + end_pattern = f'' + tag_stack = [] + chunk_start = next_tag_start + current_pos = next_tag_start + + while current_pos < len(content): + # Look for next start or end tag of the same type + next_start = content.find(f'<{current_tag}', current_pos + 1) + next_end = content.find(end_pattern, current_pos) + + if next_end == -1: # No closing tag found + break + + if next_start != -1 and next_start < next_end: + # Found nested start tag + tag_stack.append(next_start) + current_pos = next_start + 1 + else: + # Found end tag + if not tag_stack: # This is our matching end tag + chunk_end = next_end + len(end_pattern) + chunk = content[chunk_start:chunk_end] + chunks.append(chunk) + pos = chunk_end + break + else: + # Pop nested tag + tag_stack.pop() + current_pos = next_end + 1 + + if current_pos >= len(content): # Reached end without finding closing tag + break + + pos = max(pos + 1, current_pos) + + except Exception as e: + logging.error(f"Error extracting XML chunks: {e}") + logging.error(f"Content was: {content}") + + return chunks + + async def _parse_xml_to_tool_call(self, xml_chunk: str) -> Optional[Dict[str, Any]]: + """Parse XML chunk into tool call.""" + try: + # Extract tag name and validate + tag_match = re.match(r'<([^\s>]+)', xml_chunk) + if not tag_match: + logging.error(f"No tag found in XML chunk: {xml_chunk}") + return None + + tag_name = tag_match.group(1) + logging.info(f"Found XML tag: {tag_name}") + + # Get tool info and schema + tool_info = self.tool_registry.get_xml_tool(tag_name) + if not tool_info or not tool_info['schema'].xml_schema: + logging.error(f"No tool or schema found for tag: {tag_name}") + return None + + schema = tool_info['schema'].xml_schema + params = {} + remaining_chunk = xml_chunk + + # Process each mapping + for mapping in schema.mappings: + try: + if mapping.node_type == "attribute": + # Extract attribute from opening tag + opening_tag = remaining_chunk.split('>', 1)[0] + value = self._extract_attribute(opening_tag, mapping.path) + if value is not None: + params[mapping.param_name] = value + logging.info(f"Found attribute {mapping.path} -> {mapping.param_name}: {value}") + + elif mapping.node_type == "element": + # Extract element content + content, remaining_chunk = self._extract_tag_content(remaining_chunk, mapping.path) + if content is not None: + params[mapping.param_name] = content.strip() + logging.info(f"Found element {mapping.path} -> {mapping.param_name}") + + elif mapping.node_type == "content": + if mapping.path == ".": + # Extract root content + content, _ = self._extract_tag_content(remaining_chunk, tag_name) + if content is not None: + params[mapping.param_name] = content.strip() + logging.info(f"Found root content for {mapping.param_name}") + + except Exception as e: + logging.error(f"Error processing mapping {mapping}: {e}") + continue + + # Validate required parameters + missing = [mapping.param_name for mapping in schema.mappings if mapping.param_name not in params] + if missing: + logging.error(f"Missing required parameters: {missing}") + logging.error(f"Current params: {params}") + logging.error(f"XML chunk: {xml_chunk}") + return None + + # Create tool call + tool_call = { + "id": f"tool_{hash(xml_chunk)}", + "type": "function", + "function": { + "name": tool_info['method'], + "arguments": json.dumps(params) + } + } + + logging.info(f"Created tool call: {tool_call}") + return tool_call + + except Exception as e: + logging.error(f"Error parsing XML chunk: {e}") + logging.error(f"XML chunk was: {xml_chunk}") + return None + async def parse_response(self, response: Any) -> Dict[str, Any]: response_message = response.choices[0].message content = response_message.get('content') or "" @@ -106,107 +339,4 @@ class XMLToolParser(ToolParserBase): start_idx = end_idx + len(end_tag) - return tool_calls - - def _extract_xml_chunks(self, content: str) -> List[str]: - chunks = [] - current_chunk = [] - in_tag = False - - lines = content.split('\n') - for line in lines: - # Check for registered XML tags - for tag_name in self.tool_registry.xml_tools.keys(): - if f'<{tag_name}' in line: - if in_tag: # Close previous tag if any - chunks.append('\n'.join(current_chunk)) - current_chunk = [] - in_tag = True - current_chunk = [line] - break - elif f'' in line and in_tag: - current_chunk.append(line) - chunks.append('\n'.join(current_chunk)) - current_chunk = [] - in_tag = False - break - else: - if in_tag: - current_chunk.append(line) - - if current_chunk and in_tag: - chunks.append('\n'.join(current_chunk)) - - return chunks - - async def _parse_xml_to_tool_call(self, xml_chunk: str) -> Optional[Dict[str, Any]]: - try: - # Extract tag name to look up tool - tag_match = re.match(r'<([^\s>]+)', xml_chunk) - if not tag_match: - logging.error(f"No tag found in XML chunk: {xml_chunk}") - return None - - tag_name = tag_match.group(1) - logging.info(f"Found XML tag: {tag_name}") - - tool_info = self.tool_registry.get_xml_tool(tag_name) - if not tool_info: - logging.error(f"No tool found for tag: {tag_name}") - return None - - schema = tool_info['schema'].xml_schema - if not schema: - logging.error(f"No XML schema found for tag: {tag_name}") - return None - - # Extract parameters - params = {} - - # Extract attributes - for attr_name, param_name in schema.attributes.items(): - attr_match = re.search(f'{attr_name}="([^"]+)"', xml_chunk) - if attr_match: - params[param_name] = attr_match.group(1) - logging.info(f"Found attribute {attr_name} -> {param_name}: {attr_match.group(1)}") - - # Extract mapped parameters (both direct content and nested tags) - for xml_element, param_name in schema.param_mapping.items(): - if xml_element == ".": # Root tag content - content_match = re.search(r'>(.*?)]+>$', xml_chunk, re.DOTALL) - if content_match: - content = content_match.group(1).strip() - if content: # Only set if there's actual content - params[param_name] = content - logging.info(f"Found root content for {param_name}: {content}") - else: # Nested tag - # Updated regex pattern to handle multiline content - pattern = f'<{xml_element}>(.*?)' - nested_match = re.search(pattern, xml_chunk, re.DOTALL | re.MULTILINE) - if nested_match: - params[param_name] = nested_match.group(1).strip() - logging.info(f"Found nested tag {xml_element} -> {param_name}: {nested_match.group(1)}") - - if not all(param in params for param in schema.param_mapping.values()): - missing = [param for param in schema.param_mapping.values() if param not in params] - logging.error(f"Missing required parameters: {missing}") - logging.error(f"Current params: {params}") - logging.error(f"XML chunk: {xml_chunk}") - return None - - tool_call = { - "id": f"tool_{hash(xml_chunk)}", - "type": "function", - "function": { - "name": tool_info['method'], - "arguments": json.dumps(params) - } - } - - logging.info(f"Created tool call: {tool_call}") - return tool_call - - except Exception as e: - logging.error(f"Error parsing XML chunk: {e}") - logging.error(f"XML chunk was: {xml_chunk}") - return None \ No newline at end of file + return tool_calls \ No newline at end of file