quantalogic 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantalogic/__init__.py +20 -0
- quantalogic/agent.py +638 -0
- quantalogic/agent_config.py +138 -0
- quantalogic/coding_agent.py +83 -0
- quantalogic/event_emitter.py +223 -0
- quantalogic/generative_model.py +226 -0
- quantalogic/interactive_text_editor.py +190 -0
- quantalogic/main.py +185 -0
- quantalogic/memory.py +217 -0
- quantalogic/model_names.py +19 -0
- quantalogic/print_event.py +66 -0
- quantalogic/prompts.py +99 -0
- quantalogic/server/__init__.py +3 -0
- quantalogic/server/agent_server.py +633 -0
- quantalogic/server/models.py +60 -0
- quantalogic/server/routes.py +117 -0
- quantalogic/server/state.py +199 -0
- quantalogic/server/static/js/event_visualizer.js +430 -0
- quantalogic/server/static/js/quantalogic.js +571 -0
- quantalogic/server/templates/index.html +134 -0
- quantalogic/tool_manager.py +68 -0
- quantalogic/tools/__init__.py +46 -0
- quantalogic/tools/agent_tool.py +88 -0
- quantalogic/tools/download_http_file_tool.py +64 -0
- quantalogic/tools/edit_whole_content_tool.py +70 -0
- quantalogic/tools/elixir_tool.py +240 -0
- quantalogic/tools/execute_bash_command_tool.py +116 -0
- quantalogic/tools/input_question_tool.py +57 -0
- quantalogic/tools/language_handlers/__init__.py +21 -0
- quantalogic/tools/language_handlers/c_handler.py +33 -0
- quantalogic/tools/language_handlers/cpp_handler.py +33 -0
- quantalogic/tools/language_handlers/go_handler.py +33 -0
- quantalogic/tools/language_handlers/java_handler.py +37 -0
- quantalogic/tools/language_handlers/javascript_handler.py +42 -0
- quantalogic/tools/language_handlers/python_handler.py +29 -0
- quantalogic/tools/language_handlers/rust_handler.py +33 -0
- quantalogic/tools/language_handlers/scala_handler.py +33 -0
- quantalogic/tools/language_handlers/typescript_handler.py +42 -0
- quantalogic/tools/list_directory_tool.py +123 -0
- quantalogic/tools/llm_tool.py +119 -0
- quantalogic/tools/markitdown_tool.py +105 -0
- quantalogic/tools/nodejs_tool.py +515 -0
- quantalogic/tools/python_tool.py +469 -0
- quantalogic/tools/read_file_block_tool.py +140 -0
- quantalogic/tools/read_file_tool.py +79 -0
- quantalogic/tools/replace_in_file_tool.py +300 -0
- quantalogic/tools/ripgrep_tool.py +353 -0
- quantalogic/tools/search_definition_names.py +419 -0
- quantalogic/tools/task_complete_tool.py +35 -0
- quantalogic/tools/tool.py +146 -0
- quantalogic/tools/unified_diff_tool.py +387 -0
- quantalogic/tools/write_file_tool.py +97 -0
- quantalogic/utils/__init__.py +17 -0
- quantalogic/utils/ask_user_validation.py +12 -0
- quantalogic/utils/download_http_file.py +77 -0
- quantalogic/utils/get_coding_environment.py +15 -0
- quantalogic/utils/get_environment.py +26 -0
- quantalogic/utils/get_quantalogic_rules_content.py +19 -0
- quantalogic/utils/git_ls.py +121 -0
- quantalogic/utils/read_file.py +54 -0
- quantalogic/utils/read_http_text_content.py +101 -0
- quantalogic/xml_parser.py +242 -0
- quantalogic/xml_tool_parser.py +99 -0
- quantalogic-0.2.0.dist-info/LICENSE +201 -0
- quantalogic-0.2.0.dist-info/METADATA +1034 -0
- quantalogic-0.2.0.dist-info/RECORD +68 -0
- quantalogic-0.2.0.dist-info/WHEEL +4 -0
- quantalogic-0.2.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
"""API routes for the QuantaLogic server."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import json
|
5
|
+
import uuid
|
6
|
+
from asyncio import Queue
|
7
|
+
from datetime import datetime
|
8
|
+
from typing import Empty, Optional
|
9
|
+
|
10
|
+
from fastapi import HTTPException, Request
|
11
|
+
from fastapi.responses import StreamingResponse
|
12
|
+
from fastapi.templating import Jinja2Templates
|
13
|
+
from loguru import logger
|
14
|
+
|
15
|
+
from quantalogic.server.models import TaskStatus, TaskSubmission, UserValidationResponse
|
16
|
+
from quantalogic.server.state import agent_state
|
17
|
+
|
18
|
+
# Initialize templates
|
19
|
+
templates = Jinja2Templates(directory="quantalogic/server/templates")
|
20
|
+
|
21
|
+
|
22
|
+
async def submit_validation_response(validation_id: str, response: UserValidationResponse):
|
23
|
+
"""Submit a validation response."""
|
24
|
+
if validation_id not in agent_state.validation_responses:
|
25
|
+
raise HTTPException(status_code=404, detail="Validation request not found")
|
26
|
+
|
27
|
+
response_queue = agent_state.validation_responses[validation_id]
|
28
|
+
await response_queue.put(response.response)
|
29
|
+
return {"status": "success"}
|
30
|
+
|
31
|
+
|
32
|
+
async def event_stream(request: Request, task_id: Optional[str] = None):
|
33
|
+
"""SSE endpoint for streaming agent events."""
|
34
|
+
client_id = agent_state.add_client(task_id)
|
35
|
+
|
36
|
+
try:
|
37
|
+
# Determine the appropriate queue based on task_id
|
38
|
+
if task_id:
|
39
|
+
if task_id not in agent_state.task_event_queues:
|
40
|
+
agent_state.task_event_queues[task_id] = Queue()
|
41
|
+
queue = agent_state.task_event_queues[task_id]
|
42
|
+
else:
|
43
|
+
queue = agent_state.event_queues[client_id]
|
44
|
+
|
45
|
+
async def generate():
|
46
|
+
try:
|
47
|
+
while True:
|
48
|
+
# Check for client disconnection
|
49
|
+
if await request.is_disconnected():
|
50
|
+
break
|
51
|
+
|
52
|
+
try:
|
53
|
+
# Non-blocking get with a short timeout
|
54
|
+
event = queue.get_nowait()
|
55
|
+
yield f"data: {json.dumps(event)}\n\n"
|
56
|
+
except Empty:
|
57
|
+
# Prevent tight loop and allow for disconnection checks
|
58
|
+
await asyncio.sleep(0.1)
|
59
|
+
except Exception as e:
|
60
|
+
logger.error(f"Error in event stream for client {client_id}: {e}")
|
61
|
+
finally:
|
62
|
+
# Ensure cleanup of client and task-specific resources
|
63
|
+
agent_state.remove_client(client_id)
|
64
|
+
if task_id:
|
65
|
+
agent_state.remove_task_event_queue(task_id)
|
66
|
+
|
67
|
+
return StreamingResponse(
|
68
|
+
generate(),
|
69
|
+
media_type="text/event-stream",
|
70
|
+
headers={
|
71
|
+
"Cache-Control": "no-cache",
|
72
|
+
"Connection": "keep-alive",
|
73
|
+
"Content-Type": "text/event-stream",
|
74
|
+
},
|
75
|
+
)
|
76
|
+
except Exception as e:
|
77
|
+
logger.error(f"Event stream initialization error: {e}")
|
78
|
+
agent_state.remove_client(client_id)
|
79
|
+
raise
|
80
|
+
|
81
|
+
|
82
|
+
async def get_index(request: Request):
|
83
|
+
"""Serve the main application page."""
|
84
|
+
return templates.TemplateResponse("index.html", {"request": request, "title": "QuantaLogic"})
|
85
|
+
|
86
|
+
|
87
|
+
async def submit_task(request: TaskSubmission):
|
88
|
+
"""Submit a new task and return its ID."""
|
89
|
+
task_id = str(uuid.uuid4())
|
90
|
+
agent_state.tasks[task_id] = {
|
91
|
+
"status": "pending",
|
92
|
+
"created_at": datetime.now().isoformat(),
|
93
|
+
"task": request.task,
|
94
|
+
"model_name": request.model_name,
|
95
|
+
}
|
96
|
+
return {"task_id": task_id}
|
97
|
+
|
98
|
+
|
99
|
+
async def get_task_status(task_id: str):
|
100
|
+
"""Get the status of a specific task."""
|
101
|
+
if task_id not in agent_state.tasks:
|
102
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
103
|
+
|
104
|
+
task_data = agent_state.tasks[task_id]
|
105
|
+
return TaskStatus(**task_data)
|
106
|
+
|
107
|
+
|
108
|
+
async def list_tasks(status: Optional[str] = None, limit: int = 10, offset: int = 0):
|
109
|
+
"""List all tasks with optional filtering."""
|
110
|
+
tasks = []
|
111
|
+
for task_id, task_data in agent_state.tasks.items():
|
112
|
+
if status is None or task_data["status"] == status:
|
113
|
+
tasks.append({"task_id": task_id, **task_data})
|
114
|
+
|
115
|
+
# Apply pagination
|
116
|
+
paginated_tasks = tasks[offset : offset + limit]
|
117
|
+
return {"tasks": paginated_tasks, "total": len(tasks), "limit": limit, "offset": offset}
|
@@ -0,0 +1,199 @@
|
|
1
|
+
"""State management for the QuantaLogic server."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import sys
|
5
|
+
import traceback
|
6
|
+
from datetime import datetime
|
7
|
+
from queue import Queue
|
8
|
+
from threading import Lock
|
9
|
+
from typing import Any, Dict, Optional
|
10
|
+
|
11
|
+
from loguru import logger
|
12
|
+
from rich.console import Console
|
13
|
+
|
14
|
+
# Configure logger
|
15
|
+
logger.remove()
|
16
|
+
logger.add(
|
17
|
+
sys.stderr,
|
18
|
+
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
19
|
+
level="INFO",
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
class ServerState:
|
24
|
+
"""Global server state management."""
|
25
|
+
|
26
|
+
def __init__(self):
|
27
|
+
"""Initialize the ServerState with default values for server management."""
|
28
|
+
self.interrupt_count = 0
|
29
|
+
self.force_exit = False
|
30
|
+
self.is_shutting_down = False
|
31
|
+
self.shutdown_initiated = asyncio.Event()
|
32
|
+
self.shutdown_complete = asyncio.Event()
|
33
|
+
self.server = None
|
34
|
+
|
35
|
+
async def initiate_shutdown(self, force: bool = False):
|
36
|
+
"""Initiate the shutdown process."""
|
37
|
+
if not self.is_shutting_down or force:
|
38
|
+
logger.info("Initiating server shutdown...")
|
39
|
+
self.is_shutting_down = True
|
40
|
+
self.force_exit = force
|
41
|
+
self.shutdown_initiated.set()
|
42
|
+
if force:
|
43
|
+
logger.warning("Forcing immediate shutdown...")
|
44
|
+
sys.exit(1)
|
45
|
+
await self.shutdown_complete.wait()
|
46
|
+
|
47
|
+
def handle_interrupt(self):
|
48
|
+
"""Handle interrupt signal."""
|
49
|
+
self.interrupt_count += 1
|
50
|
+
if self.interrupt_count == 1:
|
51
|
+
logger.info("Graceful shutdown initiated (press Ctrl+C again to force)")
|
52
|
+
asyncio.create_task(self.initiate_shutdown(force=False))
|
53
|
+
else:
|
54
|
+
logger.warning("Forced shutdown initiated...")
|
55
|
+
asyncio.create_task(self.initiate_shutdown(force=True))
|
56
|
+
|
57
|
+
|
58
|
+
class AgentState:
|
59
|
+
"""Manages agent state and event queues."""
|
60
|
+
|
61
|
+
def __init__(self):
|
62
|
+
"""Initialize the agent state."""
|
63
|
+
self.agent = None
|
64
|
+
self.event_queues: Dict[str, Queue] = {}
|
65
|
+
self.task_event_queues: Dict[str, Queue] = {}
|
66
|
+
self.queue_lock = Lock()
|
67
|
+
self.client_counter = 0
|
68
|
+
self.console = Console()
|
69
|
+
self.validation_requests: Dict[str, Dict[str, Any]] = {}
|
70
|
+
self.validation_responses: Dict[str, asyncio.Queue] = {}
|
71
|
+
self.tasks: Dict[str, Dict[str, Any]] = {}
|
72
|
+
self.task_queues: Dict[str, asyncio.Queue] = {}
|
73
|
+
self.agents: Dict[str, Any] = {} # Dictionary to store agents by task ID
|
74
|
+
|
75
|
+
def add_client(self, task_id: Optional[str] = None) -> str:
|
76
|
+
"""Add a new client and return its ID.
|
77
|
+
|
78
|
+
Args:
|
79
|
+
task_id (Optional[str]): Optional task ID to associate with the client.
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
str: Unique client ID
|
83
|
+
"""
|
84
|
+
with self.queue_lock:
|
85
|
+
self.client_counter += 1
|
86
|
+
client_id = f"client_{self.client_counter}"
|
87
|
+
|
88
|
+
# Create a client-specific event queue
|
89
|
+
self.event_queues[client_id] = Queue()
|
90
|
+
|
91
|
+
# If a task_id is provided, create or use an existing task-specific queue and agent
|
92
|
+
if task_id:
|
93
|
+
if task_id not in self.task_event_queues:
|
94
|
+
self.task_event_queues[task_id] = Queue()
|
95
|
+
if task_id not in self.agents:
|
96
|
+
self.agents[task_id] = self.create_agent_for_task(task_id)
|
97
|
+
|
98
|
+
logger.info(f"New client connected: {client_id} for task: {task_id}")
|
99
|
+
return client_id
|
100
|
+
|
101
|
+
def create_agent_for_task(self, task_id: str) -> Any:
|
102
|
+
"""Create and return a new agent for the specified task.
|
103
|
+
|
104
|
+
Args:
|
105
|
+
task_id (str): The task ID for which to create the agent.
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
Any: The created agent instance.
|
109
|
+
"""
|
110
|
+
# Placeholder for agent creation logic
|
111
|
+
agent = ... # Replace with actual agent creation logic
|
112
|
+
logger.info(f"Agent created for task: {task_id}")
|
113
|
+
return agent
|
114
|
+
|
115
|
+
def get_agent_for_task(self, task_id: str) -> Optional[Any]:
|
116
|
+
"""Retrieve the agent for the specified task.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
task_id (str): The task ID for which to retrieve the agent.
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Optional[Any]: The agent instance if found, else None.
|
123
|
+
"""
|
124
|
+
return self.agents.get(task_id)
|
125
|
+
|
126
|
+
def remove_client(self, client_id: str):
|
127
|
+
"""Remove a client and its event queue."""
|
128
|
+
with self.queue_lock:
|
129
|
+
if client_id in self.event_queues:
|
130
|
+
del self.event_queues[client_id]
|
131
|
+
logger.info(f"Client disconnected: {client_id}")
|
132
|
+
|
133
|
+
def _format_data_for_client(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
134
|
+
"""Format data for client consumption."""
|
135
|
+
if isinstance(data, dict):
|
136
|
+
return {k: str(v) if isinstance(v, datetime | bytes) else v for k, v in data.items()}
|
137
|
+
return data
|
138
|
+
|
139
|
+
def broadcast_event(self, event_type: str, data: Dict[str, Any]):
|
140
|
+
"""Broadcast an event to all connected clients or specific task queue.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
event_type (str): Type of the event.
|
144
|
+
data (Dict[str, Any]): Event data.
|
145
|
+
"""
|
146
|
+
from quantalogic.models import EventMessage # Import here to avoid circular dependency
|
147
|
+
|
148
|
+
try:
|
149
|
+
formatted_data = self._format_data_for_client(data)
|
150
|
+
event = EventMessage(
|
151
|
+
id=f"evt_{datetime.now().timestamp()}",
|
152
|
+
event=event_type,
|
153
|
+
task_id=data.get("task_id"),
|
154
|
+
data=formatted_data,
|
155
|
+
timestamp=datetime.now().isoformat(),
|
156
|
+
)
|
157
|
+
|
158
|
+
with self.queue_lock:
|
159
|
+
task_id = data.get("task_id")
|
160
|
+
|
161
|
+
# If task_id is provided, send to task-specific queue and use task-specific agent
|
162
|
+
if task_id and task_id in self.task_event_queues:
|
163
|
+
self.task_event_queues[task_id].put(event.model_dump())
|
164
|
+
agent = self.get_agent_for_task(task_id)
|
165
|
+
if agent:
|
166
|
+
# Use the agent for task-specific processing
|
167
|
+
# Placeholder for agent-specific logic
|
168
|
+
pass
|
169
|
+
logger.debug(f"Event sent to task-specific queue: {task_id}")
|
170
|
+
|
171
|
+
# Optionally broadcast to global event queues if needed
|
172
|
+
else:
|
173
|
+
for queue in self.event_queues.values():
|
174
|
+
queue.put(event.model_dump())
|
175
|
+
logger.debug("Event broadcast to all client queues")
|
176
|
+
|
177
|
+
except Exception as e:
|
178
|
+
logger.error(f"Error broadcasting event: {e}")
|
179
|
+
logger.error(traceback.format_exc())
|
180
|
+
|
181
|
+
def remove_task_event_queue(self, task_id: str):
|
182
|
+
"""Remove a task-specific event queue safely.
|
183
|
+
|
184
|
+
Args:
|
185
|
+
task_id (str): The task ID to remove from event queues.
|
186
|
+
"""
|
187
|
+
with self.queue_lock:
|
188
|
+
if task_id in self.task_event_queues:
|
189
|
+
del self.task_event_queues[task_id]
|
190
|
+
|
191
|
+
# Additional cleanup for related task resources
|
192
|
+
if task_id in self.tasks:
|
193
|
+
del self.tasks[task_id]
|
194
|
+
|
195
|
+
if task_id in self.task_queues:
|
196
|
+
del self.task_queues[task_id]
|
197
|
+
|
198
|
+
if task_id in self.agents:
|
199
|
+
del self.agents[task_id]
|