mirror of https://github.com/kortix-ai/suna.git
working xml v1 with nested mappings
This commit is contained in:
parent
3348dc6bb9
commit
8923b8617a
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>© 2023 Brand. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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('"', '"').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'</{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
|
Loading…
Reference in New Issue