mirror of https://github.com/kortix-ai/suna.git
xml v1 wip
This commit is contained in:
parent
512d318db3
commit
6e1739698e
|
@ -7,6 +7,9 @@ from tools.terminal_tool import TerminalTool
|
||||||
import logging
|
import logging
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
import sys
|
import sys
|
||||||
|
from agentpress.xml_tool_parser import XMLToolParser
|
||||||
|
from agentpress.xml_tool_executor import XMLToolExecutor
|
||||||
|
from agentpress.xml_results_adder import XMLResultsAdder
|
||||||
|
|
||||||
async def run_agent(thread_id: str, max_iterations: int = 5):
|
async def run_agent(thread_id: str, max_iterations: int = 5):
|
||||||
# Initialize managers and tools
|
# Initialize managers and tools
|
||||||
|
@ -47,6 +50,22 @@ async def run_agent(thread_id: str, max_iterations: int = 5):
|
||||||
iteration += 1
|
iteration += 1
|
||||||
await pre_iteration()
|
await pre_iteration()
|
||||||
|
|
||||||
|
# You are a world-class web developer who can create, edit, and delete files, and execute terminal commands. You write clean, well-structured code.
|
||||||
|
|
||||||
|
# RESPONSE FORMAT:
|
||||||
|
# Use XML tags to specify file operations:
|
||||||
|
|
||||||
|
# <create-file file_path="path/to/file">
|
||||||
|
# file contents here
|
||||||
|
# </create-file>
|
||||||
|
|
||||||
|
# <update-file file_path="path/to/file">
|
||||||
|
# updated file contents here
|
||||||
|
# </update-file>
|
||||||
|
|
||||||
|
# <delete-file file_path="path/to/file">
|
||||||
|
# </delete-file>
|
||||||
|
|
||||||
system_message = {
|
system_message = {
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": """
|
"content": """
|
||||||
|
@ -119,10 +138,12 @@ Current development environment workspace state:
|
||||||
execute_tools=True,
|
execute_tools=True,
|
||||||
stream=True,
|
stream=True,
|
||||||
immediate_tool_execution=True,
|
immediate_tool_execution=True,
|
||||||
parallel_tool_execution=True
|
parallel_tool_execution=True,
|
||||||
|
# tool_parser=XMLToolParser(),
|
||||||
|
# tool_executor=XMLToolExecutor(parallel=True),
|
||||||
|
# results_adder=XMLResultsAdder(thread_manager)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle streaming response
|
|
||||||
if isinstance(response, AsyncGenerator):
|
if isinstance(response, AsyncGenerator):
|
||||||
print("\n🤖 Assistant is responding:")
|
print("\n🤖 Assistant is responding:")
|
||||||
try:
|
try:
|
||||||
|
@ -154,12 +175,10 @@ Current development environment workspace state:
|
||||||
else:
|
else:
|
||||||
print("\n❌ Non-streaming response received:", response)
|
print("\n❌ Non-streaming response received:", response)
|
||||||
|
|
||||||
# Call after_iteration without arguments
|
|
||||||
await after_iteration()
|
await after_iteration()
|
||||||
|
|
||||||
await finalizer()
|
await finalizer()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
async def main():
|
async def main():
|
||||||
thread_manager = ThreadManager()
|
thread_manager = ThreadManager()
|
||||||
|
@ -169,7 +188,7 @@ if __name__ == "__main__":
|
||||||
thread_id,
|
thread_id,
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": "Create a Crypto Trading Bot Platform. Use modern CSS styling. Make it look like FTX, the trusted and 100% safe crypto trading platform."
|
"content": "Create a modern, responsive landing page with HTML, CSS and JS."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -123,16 +123,16 @@ class FilesTool(Tool):
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to the file to be created"
|
"description": "Path to the file to be created"
|
||||||
},
|
},
|
||||||
"content": {
|
"file_contents": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The content to write to the file"
|
"description": "The content to write to the file"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["file_path", "content"]
|
"required": ["file_path", "file_contents"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
async def create_file(self, file_path: str, content: str) -> ToolResult:
|
async def create_file(self, file_path: str, file_contents: str) -> ToolResult:
|
||||||
try:
|
try:
|
||||||
full_path = os.path.join(self.workspace, file_path)
|
full_path = os.path.join(self.workspace, file_path)
|
||||||
if os.path.exists(full_path):
|
if os.path.exists(full_path):
|
||||||
|
@ -140,7 +140,7 @@ class FilesTool(Tool):
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||||
with open(full_path, 'w') as f:
|
with open(full_path, 'w') as f:
|
||||||
f.write(content)
|
f.write(file_contents)
|
||||||
|
|
||||||
await self._update_workspace_state()
|
await self._update_workspace_state()
|
||||||
return self.success_response(f"File '{file_path}' created successfully.")
|
return self.success_response(f"File '{file_path}' created successfully.")
|
||||||
|
|
|
@ -3,88 +3,87 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CryptoBot Trading Platform</title>
|
<title>Modern Landing Page</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<script src="script.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<header>
|
||||||
<header class="navbar">
|
<nav>
|
||||||
<div class="logo">CryptoBot</div>
|
<div class="logo">Brand</div>
|
||||||
<nav>
|
<div class="menu-toggle" aria-label="Toggle mobile menu">
|
||||||
<ul>
|
<span></span>
|
||||||
<li><a href="#trade">Trade</a></li>
|
<span></span>
|
||||||
<li><a href="#bots">Trading Bots</a></li>
|
<span></span>
|
||||||
<li><a href="#wallet">Wallet</a></li>
|
|
||||||
<li><a href="#account">Account</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="user-actions">
|
|
||||||
<button class="login-btn">Login</button>
|
|
||||||
<button class="signup-btn">Sign Up</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
<ul class="nav-menu">
|
||||||
|
<li><a href="#home">Home</a></li>
|
||||||
|
<li><a href="#features">Features</a></li>
|
||||||
|
<li><a href="#about">About</a></li>
|
||||||
|
<li><a href="#contact">Contact</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
<main class="dashboard">
|
<main>
|
||||||
<section class="trading-panel">
|
<section id="home" class="hero">
|
||||||
<div class="market-overview">
|
<div class="hero-content">
|
||||||
<div class="crypto-pairs">
|
<h1>Welcome to Our Platform</h1>
|
||||||
<div class="pair active">BTC/USDT</div>
|
<p>Simplify your workflow with our innovative solution</p>
|
||||||
<div class="pair">ETH/USDT</div>
|
<a href="#" class="cta-button">Get Started</a>
|
||||||
<div class="pair">BNB/USDT</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div class="price-chart">
|
|
||||||
<div class="chart-placeholder">Price Chart Placeholder</div>
|
<section id="features" class="features">
|
||||||
</div>
|
<div class="feature-grid">
|
||||||
|
<div class="feature">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="feature-icon">
|
||||||
|
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m3.343-5.657L5.929 5.929m12.728 12.728 1.414 1.414M6.343 5.657 4.929 4.243m12.728 12.728 1.414-1.414"/>
|
||||||
|
<circle cx="12" cy="12" r="4"/>
|
||||||
|
</svg>
|
||||||
|
<h3>Efficiency</h3>
|
||||||
|
<p>Streamline your process with our first key feature</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="feature">
|
||||||
<div class="trading-interface">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="feature-icon">
|
||||||
<div class="order-types">
|
<path d="M20 7l-8-4-8 4m16 0-8 4m8-4v10l-8 4m0-10L4 7m8 4v10m0-10l8-4"/>
|
||||||
<button class="order-type active">Limit</button>
|
<circle cx="12" cy="12" r="3"/>
|
||||||
<button class="order-type">Market</button>
|
</svg>
|
||||||
<button class="order-type">Stop</button>
|
<h3>Scalability</h3>
|
||||||
</div>
|
<p>Enhance productivity with our second key feature</p>
|
||||||
<div class="order-form">
|
|
||||||
<input type="number" placeholder="Amount" class="amount-input">
|
|
||||||
<input type="number" placeholder="Price" class="price-input">
|
|
||||||
<button class="buy-btn">Buy</button>
|
|
||||||
<button class="sell-btn">Sell</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<div class="feature">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="feature-icon">
|
||||||
<section class="bot-management">
|
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
<h2>Trading Bots</h2>
|
</svg>
|
||||||
<div class="bot-list">
|
<h3>Reliability</h3>
|
||||||
<div class="bot-card">
|
<p>Optimize your workflow with our third key feature</p>
|
||||||
<h3>Trend Following Bot</h3>
|
|
||||||
<div class="bot-stats">
|
|
||||||
<span>Performance: +12.5%</span>
|
|
||||||
<span>Active</span>
|
|
||||||
</div>
|
|
||||||
<button class="configure-bot">Configure</button>
|
|
||||||
</div>
|
|
||||||
<div class="bot-card">
|
|
||||||
<h3>Arbitrage Bot</h3>
|
|
||||||
<div class="bot-stats">
|
|
||||||
<span>Performance: +8.3%</span>
|
|
||||||
<span>Inactive</span>
|
|
||||||
</div>
|
|
||||||
<button class="configure-bot">Configure</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="footer-content">
|
|
||||||
<p>© 2023 CryptoBot Trading Platform</p>
|
|
||||||
<div class="legal-links">
|
|
||||||
<a href="#">Privacy Policy</a>
|
|
||||||
<a href="#">Terms of Service</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</section>
|
||||||
</div>
|
|
||||||
<script src="script.js"></script>
|
<section id="about" class="about">
|
||||||
|
<div class="about-content">
|
||||||
|
<h2>About Us</h2>
|
||||||
|
<p>We are dedicated to creating simple, powerful solutions that transform how you work.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="contact" class="contact">
|
||||||
|
<div class="contact-form">
|
||||||
|
<h2>Contact Us</h2>
|
||||||
|
<form>
|
||||||
|
<input type="text" placeholder="Name" required>
|
||||||
|
<input type="email" placeholder="Email" required>
|
||||||
|
<textarea placeholder="Your Message" required></textarea>
|
||||||
|
<button type="submit">Send Message</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2023 Your Brand. All rights reserved.</p>
|
||||||
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,24 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const menuToggle = document.querySelector('.menu-toggle');
|
||||||
|
const navMenu = document.querySelector('.nav-menu');
|
||||||
|
|
||||||
|
menuToggle.addEventListener('click', () => {
|
||||||
|
navMenu.classList.toggle('active');
|
||||||
|
menuToggle.classList.toggle('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
const navLinks = document.querySelectorAll('.nav-menu a');
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', () => {
|
||||||
|
navMenu.classList.remove('active');
|
||||||
|
menuToggle.classList.remove('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const contactForm = document.querySelector('.contact-form form');
|
||||||
|
contactForm.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Thank you for your message! We will get back to you soon.');
|
||||||
|
contactForm.reset();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,13 +1,8 @@
|
||||||
:root {
|
:root {
|
||||||
--bg-dark: #0b0e11;
|
--primary-color: #3498db;
|
||||||
--bg-darker: #121619;
|
--secondary-color: #2ecc71;
|
||||||
--primary-color: #1b2028;
|
--text-color: #333;
|
||||||
--accent-color: #2968ff;
|
--background-color: #f4f4f4;
|
||||||
--text-light: #ffffff;
|
|
||||||
--text-muted: #8a8f9b;
|
|
||||||
--green: #02c077;
|
|
||||||
--red: #f84960;
|
|
||||||
--border-color: #2c3035;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -18,230 +13,225 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
background-color: var(--bg-dark);
|
|
||||||
color: var(--text-light);
|
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-container {
|
header {
|
||||||
max-width: 1400px;
|
position: fixed;
|
||||||
margin: 0 auto;
|
width: 100%;
|
||||||
padding: 0 15px;
|
background-color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px 0;
|
padding: 1rem 5%;
|
||||||
border-bottom: 1px solid var(--primary-color);
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .logo {
|
nav .logo {
|
||||||
font-size: 24px;
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--accent-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar nav ul {
|
nav ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar nav ul li {
|
nav ul li {
|
||||||
margin: 0 15px;
|
margin-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar nav ul li a {
|
nav ul li a {
|
||||||
color: var(--text-light);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
color: var(--text-color);
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar nav ul li a:hover {
|
nav ul li a:hover {
|
||||||
color: var(--accent-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-actions {
|
main {
|
||||||
display: flex;
|
max-width: 1200px;
|
||||||
gap: 10px;
|
margin: 0 auto;
|
||||||
}
|
padding: 0 5%;
|
||||||
|
}
|
||||||
.login-btn, .signup-btn {
|
|
||||||
padding: 8px 16px;
|
.hero {
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-btn {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--text-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.signup-btn {
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
color: var(--text-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 3fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trading-panel {
|
|
||||||
background-color: var(--bg-darker);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.market-overview {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crypto-pairs {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pair {
|
|
||||||
padding: 8px 15px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.7;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pair.active {
|
|
||||||
opacity: 1;
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-chart {
|
|
||||||
background-color: var(--bg-dark);
|
|
||||||
height: 300px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
height: 100vh;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trading-interface {
|
|
||||||
background-color: var(--bg-dark);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-types {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-type {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border: none;
|
|
||||||
color: var(--text-light);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-type.active {
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-form {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-input, .price-input {
|
|
||||||
padding: 10px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border: none;
|
|
||||||
color: var(--text-light);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buy-btn, .sell-btn {
|
|
||||||
padding: 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--text-light);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buy-btn {
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sell-btn {
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bot-management {
|
|
||||||
background-color: var(--bg-darker);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bot-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bot-card {
|
|
||||||
background-color: var(--bg-dark);
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bot-stats {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.configure-bot {
|
|
||||||
background-color: var(--accent-color);
|
|
||||||
color: var(--text-light);
|
|
||||||
border: none;
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 20px 0;
|
|
||||||
border-top: 1px solid var(--primary-color);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-links {
|
.hero-content h1 {
|
||||||
margin-top: 10px;
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-links a {
|
.hero-content p {
|
||||||
color: var(--text-muted);
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin: 0 10px;
|
border-radius: 5px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
.cta-button:hover {
|
||||||
.dashboard {
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature h3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about, .contact {
|
||||||
|
padding: 4rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form input,
|
||||||
|
.contact-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form button {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form button:hover {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-color: var(--text-color);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.menu-toggle {
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 60px;
|
||||||
|
left: 0;
|
||||||
|
background-color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu li {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-content h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle span {
|
||||||
|
height: 3px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--text-color);
|
||||||
|
margin: 3px 0;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
stroke: var(--primary-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
}
|
}
|
|
@ -356,36 +356,77 @@ class StandardToolExecutor(ToolExecutorBase):
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
# --- Results Adder Base ---
|
||||||
|
|
||||||
|
class ResultsAdderBase(ABC):
|
||||||
|
"""Abstract base class for handling tool results and message processing."""
|
||||||
|
|
||||||
|
def __init__(self, thread_manager):
|
||||||
|
"""Initialize with a ThreadManager instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
thread_manager: The ThreadManager instance to use for message operations
|
||||||
|
"""
|
||||||
|
self.add_message = thread_manager.add_message
|
||||||
|
self.update_message = thread_manager._update_message
|
||||||
|
self.list_messages = thread_manager.list_messages
|
||||||
|
self.message_added = False
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def add_initial_response(self, thread_id: str, content: str, tool_calls: Optional[List[Dict[str, Any]]] = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def update_response(self, thread_id: str, content: str, tool_calls: Optional[List[Dict[str, Any]]] = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def add_tool_result(self, thread_id: str, result: Dict[str, Any]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --- Standard Results Adder Implementation ---
|
||||||
|
|
||||||
|
class StandardResultsAdder(ResultsAdderBase):
|
||||||
|
"""Standard implementation for handling tool results and message processing."""
|
||||||
|
|
||||||
|
def __init__(self, thread_manager):
|
||||||
|
"""Initialize with ThreadManager instance."""
|
||||||
|
super().__init__(thread_manager) # Use base class initialization
|
||||||
|
|
||||||
|
async def add_initial_response(self, thread_id: str, content: str, tool_calls: Optional[List[Dict[str, Any]]] = None):
|
||||||
|
message = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": content
|
||||||
|
}
|
||||||
|
if tool_calls:
|
||||||
|
message["tool_calls"] = tool_calls
|
||||||
|
|
||||||
|
await self.add_message(thread_id, message)
|
||||||
|
self.message_added = True
|
||||||
|
|
||||||
|
async def update_response(self, thread_id: str, content: str, tool_calls: Optional[List[Dict[str, Any]]] = None):
|
||||||
|
if not self.message_added:
|
||||||
|
await self.add_initial_response(thread_id, content, tool_calls)
|
||||||
|
return
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": content
|
||||||
|
}
|
||||||
|
if tool_calls:
|
||||||
|
message["tool_calls"] = tool_calls
|
||||||
|
|
||||||
|
await self.update_message(thread_id, message)
|
||||||
|
|
||||||
|
async def add_tool_result(self, thread_id: str, result: Dict[str, Any]):
|
||||||
|
messages = await self.list_messages(thread_id)
|
||||||
|
if not any(msg.get('tool_call_id') == result['tool_call_id'] for msg in messages):
|
||||||
|
await self.add_message(thread_id, result)
|
||||||
|
|
||||||
# --- Response Processor ---
|
# --- Response Processor ---
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ProcessedResponse:
|
|
||||||
"""Container for processed LLM response data."""
|
|
||||||
content: str
|
|
||||||
tool_calls: Optional[List[Dict[str, Any]]] = None
|
|
||||||
tool_results: Optional[List[Dict[str, Any]]] = None
|
|
||||||
|
|
||||||
class StandardLLMResponseProcessor:
|
class StandardLLMResponseProcessor:
|
||||||
"""Handles LLM response processing and tool execution management.
|
"""Handles LLM response processing and tool execution management."""
|
||||||
|
|
||||||
This class coordinates the parsing of LLM responses and execution of tool calls,
|
|
||||||
managing state and message flow throughout the conversation.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
thread_id (str): Current thread identifier
|
|
||||||
tool_executor (StandardToolExecutor): Tool execution handler
|
|
||||||
tool_parser (StandardToolParser): Response parsing handler
|
|
||||||
available_functions (Dict): Available tool functions
|
|
||||||
add_message (Callable): Callback for adding messages
|
|
||||||
update_message (Callable): Callback for updating messages
|
|
||||||
list_messages (Callable): Callback for listing messages
|
|
||||||
threads_dir (str): Directory for thread storage
|
|
||||||
tool_calls_buffer (Dict): Buffer for incomplete tool calls
|
|
||||||
processed_tool_calls (Set): Set of executed tool call IDs
|
|
||||||
content_buffer (str): Buffer for accumulated content
|
|
||||||
tool_calls_accumulated (List): List of accumulated tool calls
|
|
||||||
message_added (bool): Flag for message addition status
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -395,23 +436,35 @@ class StandardLLMResponseProcessor:
|
||||||
update_message_callback: Callable = None,
|
update_message_callback: Callable = None,
|
||||||
list_messages_callback: Callable = None,
|
list_messages_callback: Callable = None,
|
||||||
parallel_tool_execution: bool = True,
|
parallel_tool_execution: bool = True,
|
||||||
threads_dir: str = "threads"
|
threads_dir: str = "threads",
|
||||||
|
tool_parser: Optional[ToolParserBase] = None,
|
||||||
|
tool_executor: Optional[ToolExecutorBase] = None,
|
||||||
|
results_adder: Optional[ResultsAdderBase] = None,
|
||||||
|
thread_manager = None # Add thread_manager parameter
|
||||||
):
|
):
|
||||||
self.thread_id = thread_id
|
self.thread_id = thread_id
|
||||||
self.tool_executor = StandardToolExecutor(parallel=parallel_tool_execution)
|
self.tool_executor = tool_executor or StandardToolExecutor(parallel=parallel_tool_execution)
|
||||||
self.tool_parser = StandardToolParser()
|
self.tool_parser = tool_parser or StandardToolParser()
|
||||||
self.available_functions = available_functions or {}
|
self.available_functions = available_functions or {}
|
||||||
self.add_message = add_message_callback
|
|
||||||
self.update_message = update_message_callback
|
|
||||||
self.list_messages = list_messages_callback
|
|
||||||
self.threads_dir = threads_dir
|
self.threads_dir = threads_dir
|
||||||
|
|
||||||
|
# Create a minimal thread manager if none provided
|
||||||
|
if thread_manager is None and (add_message_callback and update_message_callback and list_messages_callback):
|
||||||
|
class MinimalThreadManager:
|
||||||
|
def __init__(self, add_msg, update_msg, list_msg):
|
||||||
|
self.add_message = add_msg
|
||||||
|
self._update_message = update_msg
|
||||||
|
self.list_messages = list_msg
|
||||||
|
thread_manager = MinimalThreadManager(add_message_callback, update_message_callback, list_messages_callback)
|
||||||
|
|
||||||
|
# Initialize results adder
|
||||||
|
self.results_adder = results_adder or StandardResultsAdder(thread_manager)
|
||||||
|
|
||||||
# State tracking for streaming responses
|
# State tracking for streaming responses
|
||||||
self.tool_calls_buffer = {}
|
self.tool_calls_buffer = {}
|
||||||
self.processed_tool_calls = set()
|
self.processed_tool_calls = set()
|
||||||
self.content_buffer = ""
|
self.content_buffer = ""
|
||||||
self.tool_calls_accumulated = []
|
self.tool_calls_accumulated = []
|
||||||
self.message_added = False
|
|
||||||
|
|
||||||
async def process_stream(
|
async def process_stream(
|
||||||
self,
|
self,
|
||||||
|
@ -419,23 +472,17 @@ class StandardLLMResponseProcessor:
|
||||||
execute_tools: bool = True,
|
execute_tools: bool = True,
|
||||||
immediate_execution: bool = True
|
immediate_execution: bool = True
|
||||||
) -> AsyncGenerator:
|
) -> AsyncGenerator:
|
||||||
"""
|
"""Process streaming LLM response and handle tool execution."""
|
||||||
Process streaming LLM response and handle tool execution.
|
|
||||||
Yields chunks immediately while managing message state and tool execution efficiently.
|
|
||||||
"""
|
|
||||||
pending_tool_calls = []
|
pending_tool_calls = []
|
||||||
background_tasks = set()
|
background_tasks = set()
|
||||||
tool_results = [] # Track tool results
|
|
||||||
|
|
||||||
async def handle_message_management(chunk):
|
async def handle_message_management(chunk):
|
||||||
try:
|
try:
|
||||||
nonlocal tool_results
|
|
||||||
|
|
||||||
# Accumulate content
|
# Accumulate content
|
||||||
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content:
|
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content:
|
||||||
self.content_buffer += chunk.choices[0].delta.content
|
self.content_buffer += chunk.choices[0].delta.content
|
||||||
|
|
||||||
# Parse tool calls only if present in chunk
|
# Parse tool calls if present
|
||||||
if hasattr(chunk.choices[0].delta, 'tool_calls'):
|
if hasattr(chunk.choices[0].delta, 'tool_calls'):
|
||||||
parsed_message, is_complete = await self.tool_parser.parse_stream(
|
parsed_message, is_complete = await self.tool_parser.parse_stream(
|
||||||
chunk,
|
chunk,
|
||||||
|
@ -451,26 +498,21 @@ class StandardLLMResponseProcessor:
|
||||||
if tool_call['id'] not in self.processed_tool_calls
|
if tool_call['id'] not in self.processed_tool_calls
|
||||||
]
|
]
|
||||||
|
|
||||||
if new_tool_calls and immediate_execution:
|
if new_tool_calls:
|
||||||
results = await self.tool_executor.execute_tool_calls(
|
if immediate_execution:
|
||||||
tool_calls=new_tool_calls,
|
results = await self.tool_executor.execute_tool_calls(
|
||||||
available_functions=self.available_functions,
|
tool_calls=new_tool_calls,
|
||||||
thread_id=self.thread_id,
|
available_functions=self.available_functions,
|
||||||
executed_tool_calls=self.processed_tool_calls
|
thread_id=self.thread_id,
|
||||||
)
|
executed_tool_calls=self.processed_tool_calls
|
||||||
tool_results.extend(results)
|
)
|
||||||
for result in results:
|
for result in results:
|
||||||
self.processed_tool_calls.add(result['tool_call_id'])
|
await self.results_adder.add_tool_result(self.thread_id, result)
|
||||||
elif new_tool_calls:
|
self.processed_tool_calls.add(result['tool_call_id'])
|
||||||
pending_tool_calls.extend(new_tool_calls)
|
else:
|
||||||
|
pending_tool_calls.extend(new_tool_calls)
|
||||||
|
|
||||||
for result in tool_results:
|
# Add/update assistant message
|
||||||
if not any(msg.get('tool_call_id') == result['tool_call_id']
|
|
||||||
for msg in await self.list_messages(self.thread_id)):
|
|
||||||
await self.add_message(self.thread_id, result)
|
|
||||||
tool_results = [] # Clear processed results
|
|
||||||
|
|
||||||
# Then add/update assistant message
|
|
||||||
message = {
|
message = {
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": self.content_buffer
|
"content": self.content_buffer
|
||||||
|
@ -478,11 +520,19 @@ class StandardLLMResponseProcessor:
|
||||||
if self.tool_calls_accumulated:
|
if self.tool_calls_accumulated:
|
||||||
message["tool_calls"] = self.tool_calls_accumulated
|
message["tool_calls"] = self.tool_calls_accumulated
|
||||||
|
|
||||||
if not self.message_added:
|
if not hasattr(self, '_message_added'):
|
||||||
await self.add_message(self.thread_id, message)
|
await self.results_adder.add_initial_response(
|
||||||
self.message_added = True
|
self.thread_id,
|
||||||
|
self.content_buffer,
|
||||||
|
self.tool_calls_accumulated
|
||||||
|
)
|
||||||
|
self._message_added = True
|
||||||
else:
|
else:
|
||||||
await self.update_message(self.thread_id, message)
|
await self.results_adder.update_response(
|
||||||
|
self.thread_id,
|
||||||
|
self.content_buffer,
|
||||||
|
self.tool_calls_accumulated
|
||||||
|
)
|
||||||
|
|
||||||
# Handle stream completion
|
# Handle stream completion
|
||||||
if chunk.choices[0].finish_reason:
|
if chunk.choices[0].finish_reason:
|
||||||
|
@ -494,50 +544,39 @@ class StandardLLMResponseProcessor:
|
||||||
executed_tool_calls=self.processed_tool_calls
|
executed_tool_calls=self.processed_tool_calls
|
||||||
)
|
)
|
||||||
for result in results:
|
for result in results:
|
||||||
await self.add_message(self.thread_id, result)
|
await self.results_adder.add_tool_result(self.thread_id, result)
|
||||||
self.processed_tool_calls.add(result['tool_call_id'])
|
self.processed_tool_calls.add(result['tool_call_id'])
|
||||||
pending_tool_calls.clear()
|
pending_tool_calls.clear()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in background task: {e}")
|
logging.error(f"Error in background task: {e}")
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for chunk in response_stream:
|
async for chunk in response_stream:
|
||||||
# Create and track background task
|
|
||||||
task = asyncio.create_task(handle_message_management(chunk))
|
task = asyncio.create_task(handle_message_management(chunk))
|
||||||
background_tasks.add(task)
|
background_tasks.add(task)
|
||||||
task.add_done_callback(background_tasks.discard)
|
task.add_done_callback(background_tasks.discard)
|
||||||
|
|
||||||
# Immediately yield the chunk
|
|
||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
# Wait for all background tasks to complete
|
|
||||||
if background_tasks:
|
if background_tasks:
|
||||||
await asyncio.gather(*background_tasks, return_exceptions=True)
|
await asyncio.gather(*background_tasks, return_exceptions=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error in stream processing: {e}")
|
logging.error(f"Error in stream processing: {e}")
|
||||||
# Clean up any remaining background tasks
|
|
||||||
for task in background_tasks:
|
for task in background_tasks:
|
||||||
if not task.done():
|
if not task.done():
|
||||||
task.cancel()
|
task.cancel()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def process_response(
|
async def process_response(self, response: Any, execute_tools: bool = True) -> None:
|
||||||
self,
|
"""Process complete LLM response and execute tools."""
|
||||||
response: Any,
|
|
||||||
execute_tools: bool = True
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Process complete LLM response and execute tools.
|
|
||||||
|
|
||||||
Handles non-streaming responses, parsing the complete response and
|
|
||||||
executing any tool calls according to the configured execution strategy.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
assistant_message = await self.tool_parser.parse_response(response)
|
assistant_message = await self.tool_parser.parse_response(response)
|
||||||
await self.add_message(self.thread_id, assistant_message)
|
await self.results_adder.add_initial_response(
|
||||||
|
self.thread_id,
|
||||||
|
assistant_message['content'],
|
||||||
|
assistant_message.get('tool_calls')
|
||||||
|
)
|
||||||
|
|
||||||
if execute_tools and 'tool_calls' in assistant_message and assistant_message['tool_calls']:
|
if execute_tools and 'tool_calls' in assistant_message and assistant_message['tool_calls']:
|
||||||
results = await self.tool_executor.execute_tool_calls(
|
results = await self.tool_executor.execute_tool_calls(
|
||||||
|
@ -548,31 +587,10 @@ class StandardLLMResponseProcessor:
|
||||||
)
|
)
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
await self.add_message(self.thread_id, result)
|
await self.results_adder.add_tool_result(self.thread_id, result)
|
||||||
logging.info(f"Tool execution result: {result}")
|
logging.info(f"Tool execution result: {result}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error processing response: {e}")
|
logging.error(f"Error processing response: {e}")
|
||||||
response_content = response.choices[0].message.get('content', '')
|
response_content = response.choices[0].message.get('content', '')
|
||||||
await self.add_message(self.thread_id, {
|
await self.results_adder.add_initial_response(self.thread_id, response_content)
|
||||||
"role": "assistant",
|
|
||||||
"content": response_content or ""
|
|
||||||
})
|
|
||||||
|
|
||||||
class ThreadManager:
|
|
||||||
"""Manages conversation threads with LLM models and tool execution.
|
|
||||||
|
|
||||||
The ThreadManager provides comprehensive conversation management, handling
|
|
||||||
message threading, tool registration, and LLM interactions.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
threads_dir (str): Directory for storing thread files
|
|
||||||
tool_registry (ToolRegistry): Registry for managing available tools
|
|
||||||
|
|
||||||
Key Features:
|
|
||||||
- Thread creation and management
|
|
||||||
- Message handling with support for text and images
|
|
||||||
- Tool registration and execution
|
|
||||||
- LLM interaction with streaming support
|
|
||||||
- Error handling and cleanup
|
|
||||||
"""
|
|
||||||
|
|
|
@ -6,6 +6,9 @@ from agentpress.llm import make_llm_api_call
|
||||||
from agentpress.tool import Tool, ToolResult
|
from agentpress.tool import Tool, ToolResult
|
||||||
from agentpress.tool_registry import ToolRegistry
|
from agentpress.tool_registry import ToolRegistry
|
||||||
from agentpress.thread_llm_response_processor import StandardLLMResponseProcessor
|
from agentpress.thread_llm_response_processor import StandardLLMResponseProcessor
|
||||||
|
from agentpress.thread_llm_response_processor import ToolParserBase
|
||||||
|
from agentpress.thread_llm_response_processor import ToolExecutorBase
|
||||||
|
from agentpress.thread_llm_response_processor import ResultsAdderBase
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
class ThreadManager:
|
class ThreadManager:
|
||||||
|
@ -218,45 +221,21 @@ class ThreadManager:
|
||||||
execute_tools: bool = True,
|
execute_tools: bool = True,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
immediate_tool_execution: bool = True,
|
immediate_tool_execution: bool = True,
|
||||||
parallel_tool_execution: bool = True
|
parallel_tool_execution: bool = True,
|
||||||
|
tool_parser: Optional[ToolParserBase] = None,
|
||||||
|
tool_executor: Optional[ToolExecutorBase] = None,
|
||||||
|
results_adder: Optional[ResultsAdderBase] = None
|
||||||
) -> Union[Dict[str, Any], AsyncGenerator]:
|
) -> Union[Dict[str, Any], AsyncGenerator]:
|
||||||
"""Run a conversation thread with specified parameters.
|
"""Run a conversation thread with specified parameters."""
|
||||||
|
|
||||||
Executes a conversation turn with the LLM, handling tool execution
|
|
||||||
and response processing based on the provided configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
thread_id: Target thread identifier
|
|
||||||
system_message: System context message
|
|
||||||
model_name: LLM model identifier
|
|
||||||
temperature: Model temperature setting
|
|
||||||
max_tokens: Maximum response length
|
|
||||||
tool_choice: Tool selection mode
|
|
||||||
temporary_message: Optional temporary context
|
|
||||||
use_tools: Enable tool usage
|
|
||||||
execute_tools: Enable tool execution
|
|
||||||
stream: Enable response streaming
|
|
||||||
immediate_tool_execution: Execute tools immediately
|
|
||||||
parallel_tool_execution: Enable parallel execution
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Union[Dict[str, Any], AsyncGenerator]: Response data or stream
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: For execution failures
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Get thread messages and prepare for LLM call
|
|
||||||
messages = await self.list_messages(thread_id)
|
messages = await self.list_messages(thread_id)
|
||||||
prepared_messages = [system_message] + messages
|
prepared_messages = [system_message] + messages
|
||||||
if temporary_message:
|
if temporary_message:
|
||||||
prepared_messages.append(temporary_message)
|
prepared_messages.append(temporary_message)
|
||||||
|
|
||||||
# Configure tools if enabled
|
|
||||||
tools = self.tool_registry.get_all_tool_schemas() if use_tools else None
|
tools = self.tool_registry.get_all_tool_schemas() if use_tools else None
|
||||||
available_functions = self.tool_registry.get_available_functions() if use_tools else {}
|
available_functions = self.tool_registry.get_available_functions() if use_tools else {}
|
||||||
|
|
||||||
# Initialize response processor with list_messages callback
|
|
||||||
response_processor = StandardLLMResponseProcessor(
|
response_processor = StandardLLMResponseProcessor(
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
available_functions=available_functions,
|
available_functions=available_functions,
|
||||||
|
@ -264,10 +243,12 @@ class ThreadManager:
|
||||||
update_message_callback=self._update_message,
|
update_message_callback=self._update_message,
|
||||||
list_messages_callback=self.list_messages,
|
list_messages_callback=self.list_messages,
|
||||||
parallel_tool_execution=parallel_tool_execution,
|
parallel_tool_execution=parallel_tool_execution,
|
||||||
threads_dir=self.threads_dir
|
threads_dir=self.threads_dir,
|
||||||
|
tool_parser=tool_parser, # Use provided parser or default to Standard
|
||||||
|
tool_executor=tool_executor, # Use provided executor or default to Standard
|
||||||
|
results_adder=results_adder # Use provided adder or default to Standard
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get LLM response
|
|
||||||
llm_response = await self._run_thread_completion(
|
llm_response = await self._run_thread_completion(
|
||||||
messages=prepared_messages,
|
messages=prepared_messages,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
|
@ -285,7 +266,6 @@ class ThreadManager:
|
||||||
immediate_execution=immediate_tool_execution
|
immediate_execution=immediate_tool_execution
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process non-streaming response
|
|
||||||
await response_processor.process_response(
|
await response_processor.process_response(
|
||||||
response=llm_response,
|
response=llm_response,
|
||||||
execute_tools=execute_tools
|
execute_tools=execute_tools
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
from agentpress.thread_llm_response_processor import ResultsAdderBase
|
||||||
|
|
||||||
|
class XMLResultsAdder(ResultsAdderBase):
|
||||||
|
"""XML-specific implementation for handling tool results and message processing.
|
||||||
|
|
||||||
|
This implementation combines tool calls and their results into a single XML-formatted
|
||||||
|
message, avoiding the need for separate tool_calls and tool_results messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, thread_manager):
|
||||||
|
super().__init__(thread_manager)
|
||||||
|
self.pending_tool_results = {}
|
||||||
|
|
||||||
|
def _format_xml_response(self, content: str, tool_calls: Optional[List[Dict[str, Any]]] = None) -> str:
|
||||||
|
"""Format the response content with XML tool results."""
|
||||||
|
response_parts = []
|
||||||
|
|
||||||
|
# Add any non-XML content first
|
||||||
|
non_xml_content = []
|
||||||
|
lines = content.split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if not (line.strip().startswith('<') and line.strip().endswith('>')):
|
||||||
|
non_xml_content.append(line)
|
||||||
|
if non_xml_content:
|
||||||
|
response_parts.append('\n'.join(non_xml_content))
|
||||||
|
|
||||||
|
# Add XML blocks with their results
|
||||||
|
if tool_calls:
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
tool_id = tool_call['id']
|
||||||
|
if tool_id in self.pending_tool_results:
|
||||||
|
result = self.pending_tool_results[tool_id]
|
||||||
|
response_parts.append(
|
||||||
|
f"<tool-result id='{tool_id}'>\n"
|
||||||
|
f"{result}\n"
|
||||||
|
f"</tool-result>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return '\n\n'.join(response_parts)
|
||||||
|
|
||||||
|
async def add_initial_response(self, thread_id: str, content: str, tool_calls: Optional[List[Dict[str, Any]]] = None):
|
||||||
|
"""Add initial response with XML formatting."""
|
||||||
|
formatted_content = self._format_xml_response(content, tool_calls)
|
||||||
|
message = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": formatted_content
|
||||||
|
}
|
||||||
|
await self.add_message(thread_id, message)
|
||||||
|
self.message_added = True
|
||||||
|
|
||||||
|
async def update_response(self, thread_id: str, content: str, tool_calls: Optional[List[Dict[str, Any]]] = None):
|
||||||
|
"""Update response with XML formatting."""
|
||||||
|
if not self.message_added:
|
||||||
|
await self.add_initial_response(thread_id, content, tool_calls)
|
||||||
|
return
|
||||||
|
|
||||||
|
formatted_content = self._format_xml_response(content, tool_calls)
|
||||||
|
message = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": formatted_content
|
||||||
|
}
|
||||||
|
await self.update_message(thread_id, message)
|
||||||
|
|
||||||
|
async def add_tool_result(self, thread_id: str, result: Dict[str, Any]):
|
||||||
|
"""Store tool result for inclusion in the XML response."""
|
||||||
|
tool_call_id = result['tool_call_id']
|
||||||
|
self.pending_tool_results[tool_call_id] = result['content']
|
||||||
|
|
||||||
|
# Update the message to include the new result
|
||||||
|
messages = await self.list_messages(thread_id)
|
||||||
|
for msg in reversed(messages):
|
||||||
|
if msg['role'] == 'assistant':
|
||||||
|
content = msg['content']
|
||||||
|
tool_calls = msg.get('tool_calls', [])
|
||||||
|
await self.update_response(thread_id, content, tool_calls)
|
||||||
|
break
|
|
@ -0,0 +1,131 @@
|
||||||
|
from typing import List, Dict, Any, Set, Callable, Optional
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from agentpress.thread_llm_response_processor import ToolExecutorBase
|
||||||
|
from agentpress.tool import ToolResult
|
||||||
|
|
||||||
|
class XMLToolExecutor(ToolExecutorBase):
|
||||||
|
def __init__(self, parallel: bool = True):
|
||||||
|
self.parallel = parallel
|
||||||
|
|
||||||
|
async def execute_tool_calls(
|
||||||
|
self,
|
||||||
|
tool_calls: List[Dict[str, Any]],
|
||||||
|
available_functions: Dict[str, Callable],
|
||||||
|
thread_id: str,
|
||||||
|
executed_tool_calls: Optional[Set[str]] = None
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
if executed_tool_calls is None:
|
||||||
|
executed_tool_calls = set()
|
||||||
|
|
||||||
|
if self.parallel:
|
||||||
|
return await self._execute_parallel(
|
||||||
|
tool_calls,
|
||||||
|
available_functions,
|
||||||
|
thread_id,
|
||||||
|
executed_tool_calls
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return await self._execute_sequential(
|
||||||
|
tool_calls,
|
||||||
|
available_functions,
|
||||||
|
thread_id,
|
||||||
|
executed_tool_calls
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _execute_parallel(
|
||||||
|
self,
|
||||||
|
tool_calls: List[Dict[str, Any]],
|
||||||
|
available_functions: Dict[str, Callable],
|
||||||
|
thread_id: str,
|
||||||
|
executed_tool_calls: Set[str]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
async def execute_single_tool(tool_call: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
if tool_call['id'] in executed_tool_calls:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
function_name = tool_call['function']['name']
|
||||||
|
function_args = tool_call['function']['arguments']
|
||||||
|
if isinstance(function_args, str):
|
||||||
|
function_args = json.loads(function_args)
|
||||||
|
|
||||||
|
function_to_call = available_functions.get(function_name)
|
||||||
|
if not function_to_call:
|
||||||
|
error_msg = f"Function {function_name} not found"
|
||||||
|
logging.error(error_msg)
|
||||||
|
return {
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call['id'],
|
||||||
|
"name": function_name,
|
||||||
|
"content": str(ToolResult(success=False, output=error_msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await function_to_call(**function_args)
|
||||||
|
executed_tool_calls.add(tool_call['id'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call['id'],
|
||||||
|
"name": function_name,
|
||||||
|
"content": str(result)
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error executing {function_name}: {str(e)}"
|
||||||
|
logging.error(error_msg)
|
||||||
|
return {
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call['id'],
|
||||||
|
"name": function_name,
|
||||||
|
"content": str(ToolResult(success=False, output=error_msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks = [execute_single_tool(tool_call) for tool_call in tool_calls]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
return [r for r in results if r is not None]
|
||||||
|
|
||||||
|
async def _execute_sequential(
|
||||||
|
self,
|
||||||
|
tool_calls: List[Dict[str, Any]],
|
||||||
|
available_functions: Dict[str, Callable],
|
||||||
|
thread_id: str,
|
||||||
|
executed_tool_calls: Set[str]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
results = []
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
if tool_call['id'] in executed_tool_calls:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
function_name = tool_call['function']['name']
|
||||||
|
function_args = tool_call['function']['arguments']
|
||||||
|
if isinstance(function_args, str):
|
||||||
|
function_args = json.loads(function_args)
|
||||||
|
|
||||||
|
function_to_call = available_functions.get(function_name)
|
||||||
|
if not function_to_call:
|
||||||
|
error_msg = f"Function {function_name} not found"
|
||||||
|
logging.error(error_msg)
|
||||||
|
result = ToolResult(success=False, output=error_msg)
|
||||||
|
else:
|
||||||
|
result = await function_to_call(**function_args)
|
||||||
|
executed_tool_calls.add(tool_call['id'])
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call['id'],
|
||||||
|
"name": function_name,
|
||||||
|
"content": str(result)
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Error executing {function_name}: {str(e)}"
|
||||||
|
logging.error(error_msg)
|
||||||
|
results.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call['id'],
|
||||||
|
"name": function_name,
|
||||||
|
"content": str(ToolResult(success=False, output=error_msg))
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
|
@ -0,0 +1,189 @@
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from agentpress.thread_llm_response_processor import ToolParserBase
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
class XMLToolParser(ToolParserBase):
|
||||||
|
def __init__(self):
|
||||||
|
self.current_tag = None
|
||||||
|
self.current_content = []
|
||||||
|
self.file_path = None
|
||||||
|
|
||||||
|
async def parse_response(self, response: Any) -> Dict[str, Any]:
|
||||||
|
response_message = response.choices[0].message
|
||||||
|
content = response_message.get('content') or ""
|
||||||
|
|
||||||
|
message = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
|
||||||
|
tool_calls = []
|
||||||
|
try:
|
||||||
|
xml_chunks = self._extract_xml_chunks(content)
|
||||||
|
for xml_chunk in xml_chunks:
|
||||||
|
tool_call = self._parse_xml_to_tool_call(xml_chunk)
|
||||||
|
if tool_call:
|
||||||
|
tool_calls.append(tool_call)
|
||||||
|
|
||||||
|
if tool_calls:
|
||||||
|
message["tool_calls"] = tool_calls
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error parsing XML response: {e}")
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
async def parse_stream(self, response_chunk: Any, tool_calls_buffer: Dict[int, Dict]) -> tuple[Optional[Dict[str, Any]], bool]:
|
||||||
|
content_chunk = ""
|
||||||
|
is_complete = False
|
||||||
|
|
||||||
|
if hasattr(response_chunk.choices[0], 'delta'):
|
||||||
|
delta = response_chunk.choices[0].delta
|
||||||
|
|
||||||
|
if hasattr(delta, 'content') and delta.content:
|
||||||
|
content_chunk = delta.content
|
||||||
|
tool_calls_buffer.setdefault('xml_buffer', '')
|
||||||
|
tool_calls_buffer['xml_buffer'] += content_chunk
|
||||||
|
|
||||||
|
# Process any complete XML tags
|
||||||
|
tool_calls = self._process_streaming_xml(tool_calls_buffer['xml_buffer'])
|
||||||
|
if tool_calls:
|
||||||
|
# Clear processed content from buffer
|
||||||
|
last_end_tag = max(
|
||||||
|
tool_calls_buffer['xml_buffer'].rfind('</create-file>'),
|
||||||
|
tool_calls_buffer['xml_buffer'].rfind('</update-file>'),
|
||||||
|
tool_calls_buffer['xml_buffer'].rfind('</delete-file>')
|
||||||
|
)
|
||||||
|
if last_end_tag > -1:
|
||||||
|
tool_calls_buffer['xml_buffer'] = tool_calls_buffer['xml_buffer'][last_end_tag + 1:]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": content_chunk,
|
||||||
|
"tool_calls": tool_calls
|
||||||
|
}, is_complete
|
||||||
|
|
||||||
|
if hasattr(response_chunk.choices[0], 'finish_reason') and response_chunk.choices[0].finish_reason:
|
||||||
|
is_complete = True
|
||||||
|
if 'xml_buffer' in tool_calls_buffer:
|
||||||
|
tool_calls = self._process_streaming_xml(tool_calls_buffer['xml_buffer'])
|
||||||
|
if tool_calls:
|
||||||
|
return {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": content_chunk,
|
||||||
|
"tool_calls": tool_calls
|
||||||
|
}, is_complete
|
||||||
|
|
||||||
|
return None, is_complete
|
||||||
|
|
||||||
|
def _process_streaming_xml(self, content: str) -> list[Dict[str, Any]]:
|
||||||
|
tool_calls = []
|
||||||
|
|
||||||
|
# Find complete XML tags
|
||||||
|
start_tags = ['<create-file', '<update-file', '<delete-file']
|
||||||
|
end_tags = ['</create-file>', '</update-file>', '</delete-file>']
|
||||||
|
|
||||||
|
for start_tag in start_tags:
|
||||||
|
start_idx = content.find(start_tag)
|
||||||
|
if start_idx >= 0:
|
||||||
|
# Find corresponding end tag
|
||||||
|
tag_type = start_tag[1:] # Remove '<'
|
||||||
|
end_tag = f"</{tag_type}>"
|
||||||
|
end_idx = content.find(end_tag, start_idx)
|
||||||
|
|
||||||
|
if end_idx >= 0:
|
||||||
|
# Extract complete XML chunk
|
||||||
|
xml_chunk = content[start_idx:end_idx + len(end_tag)]
|
||||||
|
try:
|
||||||
|
tool_call = self._parse_xml_to_tool_call(xml_chunk)
|
||||||
|
if tool_call:
|
||||||
|
tool_calls.append(tool_call)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error parsing streaming XML chunk: {e}")
|
||||||
|
|
||||||
|
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:
|
||||||
|
if any(tag in line for tag in ['<create-file', '<update-file', '<delete-file']):
|
||||||
|
if in_tag: # Close previous tag if any
|
||||||
|
chunks.append('\n'.join(current_chunk))
|
||||||
|
current_chunk = []
|
||||||
|
in_tag = True
|
||||||
|
current_chunk = [line]
|
||||||
|
elif in_tag:
|
||||||
|
current_chunk.append(line)
|
||||||
|
if any(tag in line for tag in ['</create-file>', '</update-file>', '</delete-file>']):
|
||||||
|
chunks.append('\n'.join(current_chunk))
|
||||||
|
current_chunk = []
|
||||||
|
in_tag = False
|
||||||
|
|
||||||
|
if current_chunk and in_tag:
|
||||||
|
chunks.append('\n'.join(current_chunk))
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
def _parse_xml_to_tool_call(self, xml_chunk: str) -> Optional[Dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
# Extract file path from the opening tag
|
||||||
|
file_path_match = re.search(r'file_path="([^"]+)"', xml_chunk)
|
||||||
|
if not file_path_match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_path = file_path_match.group(1)
|
||||||
|
|
||||||
|
# Extract content between tags
|
||||||
|
content_match = re.search(r'>(.*?)</[^>]+>$', xml_chunk, re.DOTALL)
|
||||||
|
if not content_match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
content = content_match.group(1).strip()
|
||||||
|
|
||||||
|
# Determine operation type
|
||||||
|
if '<create-file' in xml_chunk:
|
||||||
|
return {
|
||||||
|
"id": f"tool_{hash(xml_chunk)}",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "create_file",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"file_path": file_path,
|
||||||
|
"file_contents": content
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif '<update-file' in xml_chunk:
|
||||||
|
return {
|
||||||
|
"id": f"tool_{hash(xml_chunk)}",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "str_replace",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"file_path": file_path,
|
||||||
|
"old_str": "",
|
||||||
|
"new_str": content
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif '<delete-file' in xml_chunk:
|
||||||
|
return {
|
||||||
|
"id": f"tool_{hash(xml_chunk)}",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "delete_file",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"file_path": file_path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error parsing XML chunk: {e}")
|
||||||
|
return None
|
Loading…
Reference in New Issue