working xml v1 with nested mappings

This commit is contained in:
marko-kraemer 2024-11-18 02:15:34 +01:00
parent 3348dc6bb9
commit 8923b8617a
8 changed files with 304 additions and 598 deletions

View File

@ -78,7 +78,9 @@ Use XML tags to specify file operations:
file contents here
</create-file>
<str-replace file_path="path/to/file" old_str="old_str" new_str="new_str">
<str-replace file_path="path/to/file">
<old_str>text to replace</old_str>
<new_str>replacement text</new_str>
</str-replace>
<delete-file file_path="path/to/file">
@ -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)

View File

@ -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:

View File

@ -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()

View File

@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern Landing Page</title>
<link rel="preload" href="styles.css" as="style">
<link rel="preload" href="script.js" as="script">
<link rel="stylesheet" href="styles.css">
<meta name="description" content="Innovative solutions for modern business challenges">
<meta name="keywords" content="business, technology, innovation, solutions">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚀</text></svg>">
<meta name="theme-color" content="#3498db">
</head>
<body>
<header>
<nav>
<div class="logo">Brand</div>
<div class="nav-links">
<a href="#home">Home</a>
<a href="#features">Features</a>
<a href="#about">About</a>
<a href="#contact" class="cta-button">Contact</a>
</div>
<div class="mobile-menu">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</header>
<main>
<section id="home" class="hero">
<div class="hero-content">
<h1>Transform Your Business</h1>
<p>Innovative solutions for modern challenges</p>
<a href="#features" class="primary-button">Get Started</a>
</div>
</section>
<section id="features" class="features">
<div class="feature-grid">
<div class="feature">
<svg class="feature-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
<h3>Smart Design</h3>
<p>Elegant and intuitive user interfaces</p>
</div>
<div class="feature">
<svg class="feature-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6z"/>
</svg>
<h3>Performance</h3>
<p>High-speed, optimized solutions</p>
</div>
<div class="feature">
<svg class="feature-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-8c0 1.93-1.57 3.5-3.5 3.5S8.5 13.93 8.5 12 10.07 8.5 12 8.5s3.5 1.57 3.5 3.5z"/>
</svg>
<h3>Scalability</h3>
<p>Grow without limitations</p>
</div>
</div>
</section>
<section id="about" class="about">
<div class="about-content">
<h2>About Our Company</h2>
<div class="about-grid">
<div class="about-text">
<p>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.</p>
<p>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.</p>
</div>
<div class="about-image">
<img src="https://via.placeholder.com/500x300" alt="Team Working">
</div>
</div>
</div>
</section>
<section id="contact" class="contact">
<div class="contact-container">
<h2>Contact Us</h2>
<form id="contact-form" class="contact-form">
<input type="text" name="name" placeholder="Your Name" required>
<input type="email" name="email" placeholder="Your Email" required>
<textarea name="message" placeholder="Your Message" required></textarea>
<button type="submit" class="submit-button">Send Message</button>
</form>
</div>
</section>
</main>
<footer>
<div class="footer-content">
<div class="footer-links">
<a href="#home">Home</a>
<a href="#features">Features</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
<p>&copy; 2023 Brand. All rights reserved.</p>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>

View File

@ -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);
});
});

View File

@ -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;
}
}

View File

@ -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

View File

@ -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'</{tag_name}>'
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('&quot;', '"').replace('&apos;', "'")
value = value.replace('&lt;', '<').replace('&gt;', '>')
value = value.replace('&amp;', '&')
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'</{current_tag}>'
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'</{tag_name}>' 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}>(.*?)</{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
return tool_calls