feat: 增加plan 等部分
This commit is contained in:
parent
f948dfc45f
commit
9bca0c690f
|
|
@ -598,7 +598,7 @@ LLM API 客户端:
|
||||||
|
|
||||||
### 7. 任务系统 (`services/task.py`)
|
### 7. 任务系统 (`services/task.py`)
|
||||||
|
|
||||||
用于自主任务执行和依赖管理:
|
用于自主任务执行和计划管理:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
classDiagram
|
classDiagram
|
||||||
|
|
@ -608,19 +608,24 @@ classDiagram
|
||||||
+str goal
|
+str goal
|
||||||
+TaskStatus status
|
+TaskStatus status
|
||||||
+List~Step~ steps
|
+List~Step~ steps
|
||||||
+List~Task~ subtasks
|
+str metadata
|
||||||
|
+get_step() Step?
|
||||||
|
+is_complete() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
class Step {
|
class Step {
|
||||||
+str id
|
+str id
|
||||||
+str name
|
+str name
|
||||||
|
+str goal
|
||||||
+List~str~ depends_on
|
+List~str~ depends_on
|
||||||
+StepStatus status
|
+StepStatus status
|
||||||
|
+Dict tool_calls
|
||||||
}
|
}
|
||||||
|
|
||||||
class TaskGraph {
|
class TaskGraph {
|
||||||
+topological_sort() List~Step~
|
+topological_sort() List~Step~
|
||||||
+get_ready_steps() List~Step~
|
+get_ready_steps() List~Step~
|
||||||
|
+get_parallel_levels() List~List~Step~~
|
||||||
+detect_cycles() List~List~str~~
|
+detect_cycles() List~List~str~~
|
||||||
+validate() tuple
|
+validate() tuple
|
||||||
}
|
}
|
||||||
|
|
@ -630,20 +635,26 @@ classDiagram
|
||||||
+get_task() Task
|
+get_task() Task
|
||||||
+update_task_status() Task
|
+update_task_status() Task
|
||||||
+add_steps() List~Step~
|
+add_steps() List~Step~
|
||||||
|
+set_steps_from_plan() List~Step~
|
||||||
+build_graph() TaskGraph
|
+build_graph() TaskGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Planner {
|
||||||
|
+plan() List~Dict~
|
||||||
|
+plan_sync() List~Dict~
|
||||||
|
}
|
||||||
|
|
||||||
Task "1" o-- "*" Step
|
Task "1" o-- "*" Step
|
||||||
Task "1" o-- "*" Task
|
|
||||||
TaskService ..> TaskGraph
|
TaskService ..> TaskGraph
|
||||||
|
Planner ..> TaskService
|
||||||
```
|
```
|
||||||
|
|
||||||
**任务状态 (TaskStatus):**
|
**任务状态 (TaskStatus) - 五状态模型:**
|
||||||
- `PENDING` - 待处理
|
- `PENDING` - 待处理/就绪
|
||||||
- `READY` - 就绪
|
|
||||||
- `RUNNING` - 运行中
|
- `RUNNING` - 运行中
|
||||||
- `BLOCK` - 阻塞
|
- `BLOCK` - 阻塞
|
||||||
- `TERMINATED` - 已终止
|
- `TERMINATED` - 终止
|
||||||
|
- `COMPLETED` - 完成
|
||||||
|
|
||||||
**步骤状态 (StepStatus):**
|
**步骤状态 (StepStatus):**
|
||||||
- `PENDING` - 待执行
|
- `PENDING` - 待执行
|
||||||
|
|
@ -652,6 +663,56 @@ classDiagram
|
||||||
- `FAILED` - 失败
|
- `FAILED` - 失败
|
||||||
- `SKIPPED` - 跳过
|
- `SKIPPED` - 跳过
|
||||||
|
|
||||||
|
**TaskGraph 功能:**
|
||||||
|
- `topological_sort()` - 拓扑排序获取执行顺序
|
||||||
|
- `get_ready_steps()` - 获取就绪步骤(依赖已满足)
|
||||||
|
- `get_parallel_levels()` - 获取并行执行层级
|
||||||
|
- `detect_cycles()` - 检测循环依赖
|
||||||
|
- `validate()` - 验证图结构
|
||||||
|
|
||||||
|
**Planner 功能:**
|
||||||
|
- 调用 LLM 自动分解目标为步骤
|
||||||
|
- 支持上下文(可用工具、约束条件)
|
||||||
|
- 返回结构化步骤列表
|
||||||
|
|
||||||
|
### 8. 任务执行服务 (`services/task_executor.py`)
|
||||||
|
|
||||||
|
将 Task/Plan 集成到 Agent 执行流程:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class TaskExecutorService {
|
||||||
|
+llm: LLMClient
|
||||||
|
+generate_plan() List~Dict~
|
||||||
|
+create_task_with_plan() Task
|
||||||
|
+get_execution_plan() Dict
|
||||||
|
}
|
||||||
|
|
||||||
|
class AgentWithPlan {
|
||||||
|
+chat_service
|
||||||
|
+execute_with_plan() AsyncGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskExecutorService --> TaskService
|
||||||
|
AgentWithPlan --> TaskExecutorService
|
||||||
|
AgentWithPlan --> ChatService
|
||||||
|
```
|
||||||
|
|
||||||
|
**执行流程:**
|
||||||
|
1. 用户输入目标 → AgentWithPlan 接收
|
||||||
|
2. TaskExecutorService.generate_plan() 调用 LLM 分解目标
|
||||||
|
3. 创建 Task 并设置步骤
|
||||||
|
4. 按 TaskGraph 拓扑顺序执行每个步骤
|
||||||
|
5. 每个步骤作为独立 Agent 对话执行
|
||||||
|
6. 步骤完成后更新状态,继续下一个
|
||||||
|
|
||||||
|
**SSE 事件:**
|
||||||
|
- `planning` - 计划生成中
|
||||||
|
- `plan_created` - 计划已创建
|
||||||
|
- `step_start` - 步骤开始
|
||||||
|
- `step_complete` - 步骤完成
|
||||||
|
- `task_complete` - 任务完成
|
||||||
|
|
||||||
### 7. 认证系统 (`routes/auth.py`)
|
### 7. 认证系统 (`routes/auth.py`)
|
||||||
- JWT Bearer Token
|
- JWT Bearer Token
|
||||||
- Bcrypt 密码哈希
|
- Bcrypt 密码哈希
|
||||||
|
|
@ -814,6 +875,18 @@ room_started → round_start → (message_start → message_chunk* → message_e
|
||||||
| `/tools` | GET | 可用工具列表 |
|
| `/tools` | GET | 可用工具列表 |
|
||||||
| `/tools/{name}` | GET | 工具详情 |
|
| `/tools/{name}` | GET | 工具详情 |
|
||||||
| `/tools/{name}/execute` | POST | 执行工具 |
|
| `/tools/{name}/execute` | POST | 执行工具 |
|
||||||
|
| `/tasks` | GET | 任务列表 |
|
||||||
|
| `/tasks` | POST | 创建任务 |
|
||||||
|
| `/tasks/{id}` | GET | 任务详情 |
|
||||||
|
| `/tasks/{id}` | PUT | 更新任务 |
|
||||||
|
| `/tasks/{id}` | DELETE | 删除任务 |
|
||||||
|
| `/tasks/{id}/steps` | POST | 添加步骤 |
|
||||||
|
| `/tasks/{id}/steps/{step_id}` | PUT | 更新步骤 |
|
||||||
|
| `/tasks/{id}/graph` | GET | 获取任务图 |
|
||||||
|
| `/tasks/{id}/ready-steps` | GET | 获取就绪步骤 |
|
||||||
|
| `/tasks/{id}/execute` | POST | 执行任务 |
|
||||||
|
| `/tasks/{id}/export` | GET | 导出任务 |
|
||||||
|
| `/tasks/import` | POST | 导入任务 |
|
||||||
| `/health` | GET | 健康检查 |
|
| `/health` | GET | 健康检查 |
|
||||||
| `/` | GET | 服务信息 |
|
| `/` | GET | 服务信息 |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAuth } from './utils/useAuth.js'
|
import { useAuth } from './utils/useAuth.js'
|
||||||
import AppHeader from './components/AppHeader.vue'
|
import AppHeader from './components/AppHeader.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { isLoggedIn } = useAuth()
|
const { isLoggedIn } = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 设置全局 router 引用,供 api.js 响应拦截器使用
|
||||||
|
window.__VUE_ROUTER__ = router
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -157,12 +157,11 @@ const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="no
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px 0 0;
|
margin-top: 8px;
|
||||||
|
padding-top: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
border-top: 1px solid transparent;
|
border-top: 1px solid transparent;
|
||||||
margin-top: 8px;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,8 +194,9 @@ function handleBack() {
|
||||||
<div v-if="sidebarTab === 'roomAgents' && room" class="sidebar-tab-content">
|
<div v-if="sidebarTab === 'roomAgents' && room" class="sidebar-tab-content">
|
||||||
<div class="sidebar-header sidebar-header-row">
|
<div class="sidebar-header sidebar-header-row">
|
||||||
<button class="btn-back" @click="handleBack">
|
<button class="btn-back" @click="handleBack">
|
||||||
<svg width="18" height="18">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<use href="#arrow-left-icon"/>
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
|
<polyline points="12 19 5 12 12 5"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="sidebar-title">{{ room.title }}</span>
|
<span class="sidebar-title">{{ room.title }}</span>
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,13 @@ api.interceptors.response.use(
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem('access_token')
|
localStorage.removeItem('access_token')
|
||||||
localStorage.removeItem('user')
|
localStorage.removeItem('user')
|
||||||
|
// 使用 Vue Router 跳转,避免 SPA 路由丢失
|
||||||
|
if (window.__VUE_ROUTER__) {
|
||||||
|
window.__VUE_ROUTER__.push('/auth')
|
||||||
|
} else {
|
||||||
window.location.href = '/auth'
|
window.location.href = '/auth'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return Promise.reject(error.response?.data || error.message)
|
return Promise.reject(error.response?.data || error.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -106,6 +111,7 @@ export const chatRoomsAPI = {
|
||||||
delete: (id) => api.delete(`/chat-rooms/${id}`),
|
delete: (id) => api.delete(`/chat-rooms/${id}`),
|
||||||
getMessages: (id) => api.get(`/chat-rooms/${id}/messages`),
|
getMessages: (id) => api.get(`/chat-rooms/${id}/messages`),
|
||||||
start: (id) => `/api/chat-rooms/${id}/start`,
|
start: (id) => `/api/chat-rooms/${id}/start`,
|
||||||
|
// 注意: start 返回路径字符串,由调用方使用 fetch 处理 SSE 流
|
||||||
stop: (id) => api.post(`/chat-rooms/${id}/stop`),
|
stop: (id) => api.post(`/chat-rooms/${id}/stop`),
|
||||||
reset: (id) => api.post(`/chat-rooms/${id}/reset`),
|
reset: (id) => api.post(`/chat-rooms/${id}/reset`),
|
||||||
addAgent: (roomId, data) => api.post(`/chat-rooms/${roomId}/agents`, data),
|
addAgent: (roomId, data) => api.post(`/chat-rooms/${roomId}/agents`, data),
|
||||||
|
|
|
||||||
|
|
@ -108,24 +108,24 @@ class ParallelStreamManager {
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'message_start':
|
case 'message_start':
|
||||||
store.startAgentStream(roomId, data.agent_id || data.agentId, data)
|
store.startAgentStream(roomId, data.agent_id, data)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'message_chunk':
|
case 'message_chunk':
|
||||||
store.updateAgentContent(roomId, data.agent_id || data.agentId, {
|
store.updateAgentContent(roomId, data.agent_id, {
|
||||||
content: data.content || '',
|
content: data.content || '',
|
||||||
progress: data.progress || 0
|
progress: data.progress || 0
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'message_end':
|
case 'message_end':
|
||||||
store.completeAgentStream(roomId, data.agent_id || data.agentId, data)
|
store.completeAgentStream(roomId, data.agent_id, data)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'agent_error':
|
case 'agent_error':
|
||||||
store.errorAgentStream(roomId, data.agent_id || data.agentId, {
|
store.errorAgentStream(roomId, data.agent_id, {
|
||||||
message: data.error,
|
message: data.error,
|
||||||
agentName: data.agent_name || data.agentName
|
agentName: data.agent_name
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,8 @@ export function useConversations() {
|
||||||
convMessages.value = res.data?.messages || []
|
convMessages.value = res.data?.messages || []
|
||||||
// 加载完成后强制滚动到底部(初始加载总是显示最新消息)
|
// 加载完成后强制滚动到底部(初始加载总是显示最新消息)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (typeof onInitialScroll === 'function') {
|
if (typeof initialScrollCallback === 'function') {
|
||||||
onInitialScroll()
|
initialScrollCallback()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -278,10 +278,10 @@ export function useConversations() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始滚动回调(由外部设置)
|
// 初始滚动回调(由外部设置)
|
||||||
let onInitialScroll = null
|
let initialScrollCallback = null
|
||||||
|
|
||||||
const setOnInitialScroll = (callback) => {
|
const setOnInitialScroll = (callback) => {
|
||||||
onInitialScroll = callback
|
initialScrollCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -94,14 +94,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input-area">
|
<div class="chat-input-area">
|
||||||
<input
|
<div class="input-wrapper">
|
||||||
|
<textarea
|
||||||
v-model="newMessage"
|
v-model="newMessage"
|
||||||
@keyup.enter="handleSend"
|
@keydown.enter.exact.prevent="handleSend"
|
||||||
type="text"
|
placeholder="输入消息... (Shift+Enter 换行)"
|
||||||
placeholder="输入消息..."
|
|
||||||
class="chat-input"
|
class="chat-input"
|
||||||
:disabled="sending"
|
:disabled="sending"
|
||||||
/>
|
rows="3"
|
||||||
|
></textarea>
|
||||||
<button @click="handleSend" class="btn-send" :disabled="sending || !newMessage.trim()" title="发送">
|
<button @click="handleSend" class="btn-send" :disabled="sending || !newMessage.trim()" title="发送">
|
||||||
<svg v-if="!sending" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg v-if="!sending" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||||
|
|
@ -111,6 +112,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -424,10 +426,51 @@ onUnmounted(() => {
|
||||||
.loading-messages .spinner-small { margin-bottom: 0.5rem; }
|
.loading-messages .spinner-small { margin-bottom: 0.5rem; }
|
||||||
|
|
||||||
/* 聊天输入区 */
|
/* 聊天输入区 */
|
||||||
.chat-input-area { padding: 1rem; border-top: 1px solid var(--border-light); display: flex; gap: 0.75rem; }
|
.chat-input-area { padding: 1rem; border-top: 1px solid var(--border-light); }
|
||||||
.chat-input { flex: 1; padding: 0.65rem 0.9rem; border: 1px solid var(--border-input); border-radius: 8px; background: var(--bg-input); color: var(--text-primary); font-size: 0.9rem; }
|
.input-wrapper {
|
||||||
.chat-input:focus { outline: none; border-color: var(--accent-primary); }
|
position: relative;
|
||||||
.btn-send { width: 40px; height: 40px; background: var(--accent-primary); color: white; border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s ease; }
|
display: flex;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border-input);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.input-wrapper:focus-within { border-color: var(--accent-primary); }
|
||||||
|
.chat-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem 3rem 0.75rem 1rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: none;
|
||||||
|
min-height: 72px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.chat-input:focus { outline: none; }
|
||||||
|
.chat-input:disabled { opacity: 0.6; }
|
||||||
|
.btn-send {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
.btn-send:hover:not(:disabled) { background: var(--accent-primary-hover); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3); }
|
.btn-send:hover:not(:disabled) { background: var(--accent-primary-hover); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3); }
|
||||||
.btn-send:disabled { opacity: 0.5; cursor: not-allowed; }
|
.btn-send:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""API routes module"""
|
"""API routes module"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from luxx.routes import auth, conversations, messages, tools, providers, chat_rooms, agents
|
from luxx.routes import auth, conversations, messages, tools, providers, chat_rooms, agents, tasks
|
||||||
|
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
@ -14,3 +14,4 @@ api_router.include_router(tools.router)
|
||||||
api_router.include_router(providers.router)
|
api_router.include_router(providers.router)
|
||||||
api_router.include_router(chat_rooms.router)
|
api_router.include_router(chat_rooms.router)
|
||||||
api_router.include_router(agents.router)
|
api_router.include_router(agents.router)
|
||||||
|
api_router.include_router(tasks.router)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
"""Task API routes"""
|
||||||
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from luxx.services.task import (
|
||||||
|
task_service,
|
||||||
|
TaskService,
|
||||||
|
TaskStatus,
|
||||||
|
StepStatus
|
||||||
|
)
|
||||||
|
from luxx.utils.helpers import success_response, error_response
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
||||||
|
|
||||||
|
|
||||||
|
class StepCreate(BaseModel):
|
||||||
|
"""Step creation model"""
|
||||||
|
name: str
|
||||||
|
description: str = ""
|
||||||
|
goal: str = ""
|
||||||
|
depends_on: List[str] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"name": "Read file",
|
||||||
|
"description": "Read the input file",
|
||||||
|
"goal": "Use file_read tool to read data.txt",
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TaskCreate(BaseModel):
|
||||||
|
"""Task creation model"""
|
||||||
|
name: str
|
||||||
|
goal: str
|
||||||
|
description: str = ""
|
||||||
|
steps: List[StepCreate] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"name": "Data Processing",
|
||||||
|
"goal": "Process and analyze the input data",
|
||||||
|
"description": "A complex data processing pipeline",
|
||||||
|
"steps": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TaskUpdate(BaseModel):
|
||||||
|
"""Task update model"""
|
||||||
|
name: Optional[str] = None
|
||||||
|
goal: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class StepUpdate(BaseModel):
|
||||||
|
"""Step update model"""
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
goal: Optional[str] = None
|
||||||
|
status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PlanGenerate(BaseModel):
|
||||||
|
"""Request model for plan generation"""
|
||||||
|
goal: str
|
||||||
|
context: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"goal": "Analyze sales data and generate a report",
|
||||||
|
"context": {
|
||||||
|
"available_tools": ["file_read", "python_execute"],
|
||||||
|
"constraints": "Must use Python for analysis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_service() -> TaskService:
|
||||||
|
"""Dependency to get task service"""
|
||||||
|
return task_service
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def list_tasks(service: TaskService = Depends(get_task_service)):
|
||||||
|
"""List all tasks"""
|
||||||
|
tasks = service.list_tasks()
|
||||||
|
return success_response([t.to_dict() for t in tasks])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
async def create_task(
|
||||||
|
task_data: TaskCreate,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Create a new task"""
|
||||||
|
steps = [s.model_dump() for s in task_data.steps]
|
||||||
|
task = service.create_task(
|
||||||
|
name=task_data.name,
|
||||||
|
goal=task_data.goal,
|
||||||
|
description=task_data.description,
|
||||||
|
steps=steps if steps else None
|
||||||
|
)
|
||||||
|
return success_response(task.to_dict(), "Task created successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{task_id}")
|
||||||
|
async def get_task(
|
||||||
|
task_id: str,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Get task by ID"""
|
||||||
|
task = service.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
return success_response(task.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{task_id}")
|
||||||
|
async def update_task(
|
||||||
|
task_id: str,
|
||||||
|
update_data: TaskUpdate,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Update task"""
|
||||||
|
task = service.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
|
||||||
|
if update_data.name is not None:
|
||||||
|
task.name = update_data.name
|
||||||
|
if update_data.goal is not None:
|
||||||
|
task.goal = update_data.goal
|
||||||
|
if update_data.description is not None:
|
||||||
|
task.description = update_data.description
|
||||||
|
if update_data.status is not None:
|
||||||
|
try:
|
||||||
|
task.status = TaskStatus(update_data.status)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(f"Invalid status: {update_data.status}")
|
||||||
|
|
||||||
|
task.updated_at = datetime.now()
|
||||||
|
return success_response(task.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{task_id}")
|
||||||
|
async def delete_task(
|
||||||
|
task_id: str,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Delete task"""
|
||||||
|
if not service.delete_task(task_id):
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
return success_response(message="Task deleted successfully")
|
||||||
|
|
||||||
|
|
||||||
|
# Step endpoints
|
||||||
|
|
||||||
|
@router.post("/{task_id}/steps")
|
||||||
|
async def add_steps(
|
||||||
|
task_id: str,
|
||||||
|
steps: List[StepCreate],
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Add steps to task"""
|
||||||
|
steps_data = [s.model_dump() for s in steps]
|
||||||
|
added = service.add_steps(task_id, steps_data)
|
||||||
|
if added is None:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
return success_response([s.to_dict() for s in added], "Steps added successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{task_id}/graph")
|
||||||
|
async def get_task_graph(
|
||||||
|
task_id: str,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Get task dependency graph"""
|
||||||
|
task = service.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
|
||||||
|
graph = service.build_graph(task_id)
|
||||||
|
if not graph:
|
||||||
|
return error_response("Failed to build graph")
|
||||||
|
|
||||||
|
# Validate graph
|
||||||
|
is_valid, error = graph.validate()
|
||||||
|
if not is_valid:
|
||||||
|
return error_response(f"Invalid graph: {error}")
|
||||||
|
|
||||||
|
# Get execution order
|
||||||
|
sorted_steps = graph.topological_sort()
|
||||||
|
|
||||||
|
# Get parallel execution levels
|
||||||
|
parallel_levels = graph.get_parallel_levels()
|
||||||
|
|
||||||
|
return success_response({
|
||||||
|
"task_id": task_id,
|
||||||
|
"steps": [s.to_dict() for s in task.steps],
|
||||||
|
"execution_order": [s.id for s in sorted_steps],
|
||||||
|
"parallel_levels": [[s.id for s in level] for level in parallel_levels],
|
||||||
|
"is_valid": True
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{task_id}/ready-steps")
|
||||||
|
async def get_ready_steps(
|
||||||
|
task_id: str,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Get steps that are ready to execute"""
|
||||||
|
task = service.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
|
||||||
|
graph = service.build_graph(task_id)
|
||||||
|
if not graph:
|
||||||
|
return error_response("Failed to build graph")
|
||||||
|
|
||||||
|
ready = graph.get_ready_steps()
|
||||||
|
return success_response([s.to_dict() for s in ready])
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{task_id}/steps/{step_id}")
|
||||||
|
async def update_step(
|
||||||
|
task_id: str,
|
||||||
|
step_id: str,
|
||||||
|
update_data: StepUpdate,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Update step"""
|
||||||
|
task = service.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
|
||||||
|
step = task.get_step(step_id)
|
||||||
|
if not step:
|
||||||
|
return error_response("Step not found", code=404)
|
||||||
|
|
||||||
|
if update_data.name is not None:
|
||||||
|
step.name = update_data.name
|
||||||
|
if update_data.description is not None:
|
||||||
|
step.description = update_data.description
|
||||||
|
if update_data.goal is not None:
|
||||||
|
step.goal = update_data.goal
|
||||||
|
if update_data.status is not None:
|
||||||
|
try:
|
||||||
|
step.status = StepStatus(update_data.status)
|
||||||
|
except ValueError:
|
||||||
|
return error_response(f"Invalid status: {update_data.status}")
|
||||||
|
|
||||||
|
step.updated_at = datetime.now()
|
||||||
|
task.updated_at = datetime.now()
|
||||||
|
return success_response(step.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{task_id}/execute")
|
||||||
|
async def execute_task(
|
||||||
|
task_id: str,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Mark task as running and return first ready steps"""
|
||||||
|
task = service.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
|
||||||
|
if task.status == TaskStatus.RUNNING:
|
||||||
|
return error_response("Task is already running")
|
||||||
|
|
||||||
|
# Validate graph
|
||||||
|
graph = service.build_graph(task_id)
|
||||||
|
if not graph:
|
||||||
|
return error_response("Failed to build graph")
|
||||||
|
|
||||||
|
is_valid, error = graph.validate()
|
||||||
|
if not is_valid:
|
||||||
|
return error_response(f"Invalid task graph: {error}")
|
||||||
|
|
||||||
|
# Update status to running
|
||||||
|
service.update_task_status(task_id, TaskStatus.RUNNING)
|
||||||
|
|
||||||
|
# Get ready steps
|
||||||
|
ready = graph.get_ready_steps()
|
||||||
|
if not ready:
|
||||||
|
if task.is_complete():
|
||||||
|
service.update_task_status(task_id, TaskStatus.COMPLETED)
|
||||||
|
return success_response({
|
||||||
|
"status": "completed",
|
||||||
|
"message": "All steps completed"
|
||||||
|
})
|
||||||
|
return error_response("No steps ready to execute")
|
||||||
|
|
||||||
|
return success_response({
|
||||||
|
"status": "running",
|
||||||
|
"ready_steps": [s.to_dict() for s in ready]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{task_id}/complete")
|
||||||
|
async def complete_task(
|
||||||
|
task_id: str,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Mark task as completed"""
|
||||||
|
task = service.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
|
||||||
|
if task.is_complete():
|
||||||
|
service.update_task_status(task_id, TaskStatus.COMPLETED)
|
||||||
|
return success_response({
|
||||||
|
"status": "completed",
|
||||||
|
"message": "Task completed successfully"
|
||||||
|
})
|
||||||
|
|
||||||
|
return error_response("Task is not yet complete")
|
||||||
|
|
||||||
|
|
||||||
|
# Export/Import endpoints
|
||||||
|
|
||||||
|
@router.get("/{task_id}/export")
|
||||||
|
async def export_task(
|
||||||
|
task_id: str,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Export task as JSON"""
|
||||||
|
task_json = service.export_task(task_id)
|
||||||
|
if not task_json:
|
||||||
|
return error_response("Task not found", code=404)
|
||||||
|
return success_response({"json": task_json})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/import")
|
||||||
|
async def import_task(
|
||||||
|
task_json: str,
|
||||||
|
service: TaskService = Depends(get_task_service)
|
||||||
|
):
|
||||||
|
"""Import task from JSON"""
|
||||||
|
task = service.import_task(task_json)
|
||||||
|
if not task:
|
||||||
|
return error_response("Failed to import task")
|
||||||
|
return success_response(task.to_dict(), "Task imported successfully")
|
||||||
|
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
"""Task module for autonomous task execution"""
|
"""Task module for autonomous task execution and planning"""
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any, AsyncGenerator, Callable
|
||||||
|
import json
|
||||||
|
|
||||||
from luxx.utils.helpers import generate_id
|
from luxx.utils.helpers import generate_id
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TaskStatus(Enum):
|
class TaskStatus(Enum):
|
||||||
"""Task status enum"""
|
"""Task status - 5-state model"""
|
||||||
PENDING = "pending"
|
PENDING = "pending" # 创建/就绪
|
||||||
READY = "ready"
|
RUNNING = "running" # 运行中
|
||||||
RUNNING = "running"
|
BLOCK = "block" # 阻塞
|
||||||
BLOCK = "block"
|
TERMINATED = "terminated" # 终止
|
||||||
TERMINATED = "terminated"
|
COMPLETED = "completed" # 完成
|
||||||
|
|
||||||
|
|
||||||
class StepStatus(Enum):
|
class StepStatus(Enum):
|
||||||
"""Step status enum"""
|
"""Step status"""
|
||||||
PENDING = "pending"
|
PENDING = "pending"
|
||||||
RUNNING = "running"
|
RUNNING = "running"
|
||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
|
|
@ -33,29 +35,48 @@ class Step:
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
goal: str = "" # 步骤目标(用于 LLM 执行)
|
||||||
depends_on: List[str] = field(default_factory=list)
|
depends_on: List[str] = field(default_factory=list)
|
||||||
status: StepStatus = StepStatus.PENDING
|
status: StepStatus = StepStatus.PENDING
|
||||||
result: Optional[Dict[str, Any]] = None
|
result: Optional[Dict[str, Any]] = None
|
||||||
|
tool_calls: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
context: Dict[str, Any] = field(default_factory=dict)
|
||||||
created_at: datetime = field(default_factory=datetime.now)
|
created_at: datetime = field(default_factory=datetime.now)
|
||||||
updated_at: datetime = field(default_factory=datetime.now)
|
updated_at: datetime = field(default_factory=datetime.now)
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""Convert to dictionary"""
|
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
|
"goal": self.goal,
|
||||||
"depends_on": self.depends_on,
|
"depends_on": self.depends_on,
|
||||||
"status": self.status.value,
|
"status": self.status.value,
|
||||||
"result": self.result,
|
"result": self.result,
|
||||||
|
"tool_calls": self.tool_calls,
|
||||||
|
"context": self.context,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, Any]) -> "Step":
|
||||||
|
return cls(
|
||||||
|
id=data.get("id", generate_id("step")),
|
||||||
|
name=data.get("name", ""),
|
||||||
|
description=data.get("description", ""),
|
||||||
|
goal=data.get("goal", ""),
|
||||||
|
depends_on=data.get("depends_on", []),
|
||||||
|
status=StepStatus(data.get("status", "pending")),
|
||||||
|
result=data.get("result"),
|
||||||
|
tool_calls=data.get("tool_calls", []),
|
||||||
|
context=data.get("context", {})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Task:
|
class Task:
|
||||||
"""Task entity"""
|
"""Task entity with built-in planning capability"""
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
@ -64,11 +85,11 @@ class Task:
|
||||||
steps: List[Step] = field(default_factory=list)
|
steps: List[Step] = field(default_factory=list)
|
||||||
subtasks: List["Task"] = field(default_factory=list)
|
subtasks: List["Task"] = field(default_factory=list)
|
||||||
result: Optional[Dict[str, Any]] = None
|
result: Optional[Dict[str, Any]] = None
|
||||||
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||||
created_at: datetime = field(default_factory=datetime.now)
|
created_at: datetime = field(default_factory=datetime.now)
|
||||||
updated_at: datetime = field(default_factory=datetime.now)
|
updated_at: datetime = field(default_factory=datetime.now)
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""Convert to dictionary"""
|
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
|
|
@ -78,10 +99,40 @@ class Task:
|
||||||
"steps": [s.to_dict() for s in self.steps],
|
"steps": [s.to_dict() for s in self.steps],
|
||||||
"subtasks": [t.to_dict() for t in self.subtasks],
|
"subtasks": [t.to_dict() for t in self.subtasks],
|
||||||
"result": self.result,
|
"result": self.result,
|
||||||
|
"metadata": self.metadata,
|
||||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, Any]) -> "Task":
|
||||||
|
steps = [Step.from_dict(s) if isinstance(s, dict) else s for s in data.get("steps", [])]
|
||||||
|
subtasks = [cls.from_dict(t) if isinstance(t, dict) else t for t in data.get("subtasks", [])]
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
id=data.get("id", generate_id("task")),
|
||||||
|
name=data.get("name", ""),
|
||||||
|
description=data.get("description", ""),
|
||||||
|
goal=data.get("goal", ""),
|
||||||
|
status=TaskStatus(data.get("status", "pending")),
|
||||||
|
steps=steps,
|
||||||
|
subtasks=subtasks,
|
||||||
|
result=data.get("result"),
|
||||||
|
metadata=data.get("metadata", {})
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_step(self, step_id: str) -> Optional[Step]:
|
||||||
|
for step in self.steps:
|
||||||
|
if step.id == step_id:
|
||||||
|
return step
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_completed_step_ids(self) -> List[str]:
|
||||||
|
return [s.id for s in self.steps if s.status == StepStatus.COMPLETED]
|
||||||
|
|
||||||
|
def is_complete(self) -> bool:
|
||||||
|
return all(s.status in (StepStatus.COMPLETED, StepStatus.SKIPPED) for s in self.steps)
|
||||||
|
|
||||||
|
|
||||||
class TaskGraph:
|
class TaskGraph:
|
||||||
"""Task graph for managing step dependencies"""
|
"""Task graph for managing step dependencies"""
|
||||||
|
|
@ -94,7 +145,6 @@ class TaskGraph:
|
||||||
self._build_graph()
|
self._build_graph()
|
||||||
|
|
||||||
def _build_graph(self) -> None:
|
def _build_graph(self) -> None:
|
||||||
"""Build graph from task steps"""
|
|
||||||
for step in self.task.steps:
|
for step in self.task.steps:
|
||||||
self._adjacency[step.id] = []
|
self._adjacency[step.id] = []
|
||||||
self._reverse_adjacency[step.id] = []
|
self._reverse_adjacency[step.id] = []
|
||||||
|
|
@ -108,7 +158,6 @@ class TaskGraph:
|
||||||
self._in_degree[step.id] += 1
|
self._in_degree[step.id] += 1
|
||||||
|
|
||||||
def topological_sort(self) -> List[Step]:
|
def topological_sort(self) -> List[Step]:
|
||||||
"""Get steps in topological order"""
|
|
||||||
in_degree = self._in_degree.copy()
|
in_degree = self._in_degree.copy()
|
||||||
queue = [step_id for step_id, degree in in_degree.items() if degree == 0]
|
queue = [step_id for step_id, degree in in_degree.items() if degree == 0]
|
||||||
result = []
|
result = []
|
||||||
|
|
@ -126,14 +175,14 @@ class TaskGraph:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_ready_steps(self, completed_step_ids: List[str]) -> List[Step]:
|
def get_ready_steps(self, completed_step_ids: List[str] = None) -> List[Step]:
|
||||||
"""Get steps that are ready to execute"""
|
if completed_step_ids is None:
|
||||||
|
completed_step_ids = self.task.get_completed_step_ids()
|
||||||
|
|
||||||
step_map = {step.id: step for step in self.task.steps}
|
step_map = {step.id: step for step in self.task.steps}
|
||||||
ready = []
|
ready = []
|
||||||
|
|
||||||
for step in self.task.steps:
|
for step in self.task.steps:
|
||||||
if step.id in completed_step_ids:
|
|
||||||
continue
|
|
||||||
if step.status != StepStatus.PENDING:
|
if step.status != StepStatus.PENDING:
|
||||||
continue
|
continue
|
||||||
deps_completed = all(dep_id in completed_step_ids for dep_id in step.depends_on)
|
deps_completed = all(dep_id in completed_step_ids for dep_id in step.depends_on)
|
||||||
|
|
@ -142,8 +191,30 @@ class TaskGraph:
|
||||||
|
|
||||||
return ready
|
return ready
|
||||||
|
|
||||||
|
def get_parallel_levels(self) -> List[List[Step]]:
|
||||||
|
"""Get steps grouped by dependency level for parallel execution"""
|
||||||
|
completed = set(self.task.get_completed_step_ids())
|
||||||
|
levels: List[List[Step]] = []
|
||||||
|
remaining = {s.id for s in self.task.steps if s.status == StepStatus.PENDING}
|
||||||
|
|
||||||
|
while remaining:
|
||||||
|
current_level = []
|
||||||
|
for step_id in list(remaining):
|
||||||
|
step = self.task.get_step(step_id)
|
||||||
|
if step and all(dep_id in completed for dep_id in step.depends_on):
|
||||||
|
current_level.append(step)
|
||||||
|
|
||||||
|
if not current_level:
|
||||||
|
break
|
||||||
|
|
||||||
|
levels.append(current_level)
|
||||||
|
for step in current_level:
|
||||||
|
remaining.remove(step.id)
|
||||||
|
completed.add(step.id)
|
||||||
|
|
||||||
|
return levels
|
||||||
|
|
||||||
def detect_cycles(self) -> List[List[str]]:
|
def detect_cycles(self) -> List[List[str]]:
|
||||||
"""Detect cycles in the graph"""
|
|
||||||
WHITE, GRAY, BLACK = 0, 1, 2
|
WHITE, GRAY, BLACK = 0, 1, 2
|
||||||
color = {step.id: WHITE for step in self.task.steps}
|
color = {step.id: WHITE for step in self.task.steps}
|
||||||
cycles = []
|
cycles = []
|
||||||
|
|
@ -172,7 +243,6 @@ class TaskGraph:
|
||||||
return cycles
|
return cycles
|
||||||
|
|
||||||
def validate(self) -> tuple[bool, Optional[str]]:
|
def validate(self) -> tuple[bool, Optional[str]]:
|
||||||
"""Validate the graph structure"""
|
|
||||||
cycles = self.detect_cycles()
|
cycles = self.detect_cycles()
|
||||||
if cycles:
|
if cycles:
|
||||||
return False, f"Circular dependency detected: {cycles[0]}"
|
return False, f"Circular dependency detected: {cycles[0]}"
|
||||||
|
|
@ -199,7 +269,6 @@ class TaskService:
|
||||||
description: str = "",
|
description: str = "",
|
||||||
steps: List[Dict[str, Any]] = None
|
steps: List[Dict[str, Any]] = None
|
||||||
) -> Task:
|
) -> Task:
|
||||||
"""Create a new task"""
|
|
||||||
task_id = generate_id("task")
|
task_id = generate_id("task")
|
||||||
task = Task(
|
task = Task(
|
||||||
id=task_id,
|
id=task_id,
|
||||||
|
|
@ -213,7 +282,8 @@ class TaskService:
|
||||||
step = Step(
|
step = Step(
|
||||||
id=generate_id("step"),
|
id=generate_id("step"),
|
||||||
name=step_data.get("name", ""),
|
name=step_data.get("name", ""),
|
||||||
description=step_data.get("description", "")
|
description=step_data.get("description", ""),
|
||||||
|
goal=step_data.get("goal", "")
|
||||||
)
|
)
|
||||||
task.steps.append(step)
|
task.steps.append(step)
|
||||||
|
|
||||||
|
|
@ -222,11 +292,9 @@ class TaskService:
|
||||||
return task
|
return task
|
||||||
|
|
||||||
def get_task(self, task_id: str) -> Optional[Task]:
|
def get_task(self, task_id: str) -> Optional[Task]:
|
||||||
"""Get task by ID"""
|
|
||||||
return self._tasks.get(task_id)
|
return self._tasks.get(task_id)
|
||||||
|
|
||||||
def list_tasks(self) -> List[Task]:
|
def list_tasks(self) -> List[Task]:
|
||||||
"""List all tasks"""
|
|
||||||
return list(self._tasks.values())
|
return list(self._tasks.values())
|
||||||
|
|
||||||
def update_task_status(
|
def update_task_status(
|
||||||
|
|
@ -235,7 +303,6 @@ class TaskService:
|
||||||
status: TaskStatus,
|
status: TaskStatus,
|
||||||
result: Any = None
|
result: Any = None
|
||||||
) -> Optional[Task]:
|
) -> Optional[Task]:
|
||||||
"""Update task status"""
|
|
||||||
task = self._tasks.get(task_id)
|
task = self._tasks.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
return None
|
return None
|
||||||
|
|
@ -245,12 +312,32 @@ class TaskService:
|
||||||
task.updated_at = datetime.now()
|
task.updated_at = datetime.now()
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
def update_step_status(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
step_id: str,
|
||||||
|
status: StepStatus,
|
||||||
|
result: Any = None
|
||||||
|
) -> Optional[Step]:
|
||||||
|
task = self._tasks.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return None
|
||||||
|
|
||||||
|
step = task.get_step(step_id)
|
||||||
|
if not step:
|
||||||
|
return None
|
||||||
|
|
||||||
|
step.status = status
|
||||||
|
step.result = result
|
||||||
|
step.updated_at = datetime.now()
|
||||||
|
task.updated_at = datetime.now()
|
||||||
|
return step
|
||||||
|
|
||||||
def add_steps(
|
def add_steps(
|
||||||
self,
|
self,
|
||||||
task_id: str,
|
task_id: str,
|
||||||
steps: List[Dict[str, Any]]
|
steps: List[Dict[str, Any]]
|
||||||
) -> Optional[List[Step]]:
|
) -> Optional[List[Step]]:
|
||||||
"""Add steps to task"""
|
|
||||||
task = self._tasks.get(task_id)
|
task = self._tasks.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
return None
|
return None
|
||||||
|
|
@ -261,6 +348,7 @@ class TaskService:
|
||||||
id=generate_id("step"),
|
id=generate_id("step"),
|
||||||
name=step_data.get("name", ""),
|
name=step_data.get("name", ""),
|
||||||
description=step_data.get("description", ""),
|
description=step_data.get("description", ""),
|
||||||
|
goal=step_data.get("goal", ""),
|
||||||
depends_on=step_data.get("depends_on", [])
|
depends_on=step_data.get("depends_on", [])
|
||||||
)
|
)
|
||||||
task.steps.append(step)
|
task.steps.append(step)
|
||||||
|
|
@ -269,8 +357,33 @@ class TaskService:
|
||||||
task.updated_at = datetime.now()
|
task.updated_at = datetime.now()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def set_steps_from_plan(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
plan_steps: List[Dict[str, Any]]
|
||||||
|
) -> Optional[List[Step]]:
|
||||||
|
"""Set steps from LLM-generated plan"""
|
||||||
|
task = self._tasks.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return None
|
||||||
|
|
||||||
|
task.steps = []
|
||||||
|
for step_data in plan_steps:
|
||||||
|
step = Step(
|
||||||
|
id=generate_id("step"),
|
||||||
|
name=step_data.get("name", ""),
|
||||||
|
description=step_data.get("description", ""),
|
||||||
|
goal=step_data.get("goal", step_data.get("name", "")),
|
||||||
|
depends_on=step_data.get("depends_on", [])
|
||||||
|
)
|
||||||
|
task.steps.append(step)
|
||||||
|
|
||||||
|
task.metadata["raw_plan"] = plan_steps
|
||||||
|
task.updated_at = datetime.now()
|
||||||
|
logger.info(f"Set {len(task.steps)} steps for task: {task_id}")
|
||||||
|
return task.steps
|
||||||
|
|
||||||
def delete_task(self, task_id: str) -> bool:
|
def delete_task(self, task_id: str) -> bool:
|
||||||
"""Delete task"""
|
|
||||||
if task_id not in self._tasks:
|
if task_id not in self._tasks:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -278,11 +391,159 @@ class TaskService:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def build_graph(self, task_id: str) -> Optional[TaskGraph]:
|
def build_graph(self, task_id: str) -> Optional[TaskGraph]:
|
||||||
"""Build task graph for a task"""
|
|
||||||
task = self._tasks.get(task_id)
|
task = self._tasks.get(task_id)
|
||||||
if not task:
|
if not task:
|
||||||
return None
|
return None
|
||||||
return TaskGraph(task)
|
return TaskGraph(task)
|
||||||
|
|
||||||
|
def export_task(self, task_id: str) -> Optional[str]:
|
||||||
|
task = self._tasks.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return None
|
||||||
|
return json.dumps(task.to_dict(), ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
def import_task(self, task_json: str) -> Optional[Task]:
|
||||||
|
try:
|
||||||
|
data = json.loads(task_json)
|
||||||
|
task = Task.from_dict(data)
|
||||||
|
self._tasks[task.id] = task
|
||||||
|
return task
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to import task: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Planner:
|
||||||
|
"""LLM-based task planner - generates steps from goal"""
|
||||||
|
|
||||||
|
PLANNER_SYSTEM_PROMPT = """You are a task planning assistant. Your job is to break down complex goals into clear, actionable steps.
|
||||||
|
|
||||||
|
For a given goal, you should output a JSON array of steps. Each step should have:
|
||||||
|
- name: Short, descriptive name
|
||||||
|
- description: What this step does
|
||||||
|
- depends_on: (optional) List of step indices this depends on
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Steps should be atomic and focused
|
||||||
|
2. Consider dependencies between steps
|
||||||
|
3. Order steps logically
|
||||||
|
4. Use simple dependencies when needed
|
||||||
|
|
||||||
|
Output ONLY valid JSON array, no other text."""
|
||||||
|
|
||||||
|
def __init__(self, llm_client):
|
||||||
|
self.llm = llm_client
|
||||||
|
|
||||||
|
async def plan(
|
||||||
|
self,
|
||||||
|
goal: str,
|
||||||
|
context: Dict[str, Any] = None
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate a plan (list of steps) for the given goal"""
|
||||||
|
user_message = f"Goal: {goal}\n\n"
|
||||||
|
|
||||||
|
if context:
|
||||||
|
if context.get("available_tools"):
|
||||||
|
user_message += f"Available tools: {', '.join(context['available_tools'])}\n"
|
||||||
|
if context.get("constraints"):
|
||||||
|
user_message += f"Constraints: {context['constraints']}\n"
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": self.PLANNER_SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": user_message}
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.llm.call(messages=messages)
|
||||||
|
|
||||||
|
# Parse JSON response
|
||||||
|
plan_text = response.get("content", "")
|
||||||
|
# Extract JSON from response
|
||||||
|
start = plan_text.find("[")
|
||||||
|
end = plan_text.rfind("]") + 1
|
||||||
|
if start != -1 and end > start:
|
||||||
|
plan_json = plan_text[start:end]
|
||||||
|
steps = json.loads(plan_json)
|
||||||
|
|
||||||
|
# Normalize steps - ensure depends_on uses indices
|
||||||
|
normalized_steps = []
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
normalized = {
|
||||||
|
"name": step.get("name", f"Step {i+1}"),
|
||||||
|
"description": step.get("description", ""),
|
||||||
|
"goal": step.get("goal", step.get("description", "")),
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert index dependencies to step IDs (placeholder, will be fixed later)
|
||||||
|
deps = step.get("depends_on", [])
|
||||||
|
if isinstance(deps, list):
|
||||||
|
normalized["depends_on"] = deps
|
||||||
|
|
||||||
|
normalized_steps.append(normalized)
|
||||||
|
|
||||||
|
return normalized_steps
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Planning failed: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def plan_sync(
|
||||||
|
self,
|
||||||
|
goal: str,
|
||||||
|
context: Dict[str, Any] = None
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Synchronous version of plan"""
|
||||||
|
raise NotImplementedError("Use async plan() method")
|
||||||
|
|
||||||
|
|
||||||
|
class TaskRunner:
|
||||||
|
"""Execute tasks using TaskGraph"""
|
||||||
|
|
||||||
|
def __init__(self, task_service: TaskService):
|
||||||
|
self.task_service = task_service
|
||||||
|
|
||||||
|
def run_step(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
step_id: str
|
||||||
|
) -> Optional[Step]:
|
||||||
|
"""Mark step as running"""
|
||||||
|
return self.task_service.update_step_status(
|
||||||
|
task_id, step_id, StepStatus.RUNNING
|
||||||
|
)
|
||||||
|
|
||||||
|
def complete_step(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
step_id: str,
|
||||||
|
result: Dict[str, Any] = None
|
||||||
|
) -> Optional[Step]:
|
||||||
|
"""Mark step as completed"""
|
||||||
|
return self.task_service.update_step_status(
|
||||||
|
task_id, step_id, StepStatus.COMPLETED, result
|
||||||
|
)
|
||||||
|
|
||||||
|
def fail_step(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
step_id: str,
|
||||||
|
error: str = ""
|
||||||
|
) -> Optional[Step]:
|
||||||
|
"""Mark step as failed"""
|
||||||
|
return self.task_service.update_step_status(
|
||||||
|
task_id, step_id, StepStatus.FAILED, {"error": error}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_next_steps(self, task_id: str) -> List[Step]:
|
||||||
|
"""Get steps ready to execute"""
|
||||||
|
graph = self.task_service.build_graph(task_id)
|
||||||
|
if not graph:
|
||||||
|
return []
|
||||||
|
return graph.get_ready_steps()
|
||||||
|
|
||||||
|
|
||||||
|
# Global task service instance
|
||||||
task_service = TaskService()
|
task_service = TaskService()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
"""Task Executor Service - Integrates Task with Agent execution"""
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from typing import List, Dict, Any, Optional, AsyncGenerator
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from luxx.services.task import (
|
||||||
|
task_service, TaskService, Task, Step, StepStatus, TaskStatus,
|
||||||
|
TaskGraph, Planner
|
||||||
|
)
|
||||||
|
from luxx.services.events import sse_event
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskExecutorService:
|
||||||
|
"""Integrates Task/Plan with Agent execution loop"""
|
||||||
|
|
||||||
|
# System prompt for plan generation
|
||||||
|
PLANNER_SYSTEM = """You are a task planning assistant. Break down goals into clear, executable steps.
|
||||||
|
|
||||||
|
For each step, provide:
|
||||||
|
- name: Short descriptive name
|
||||||
|
- description: What this step accomplishes
|
||||||
|
- goal: Specific objective for the agent to achieve (use this as the actual prompt)
|
||||||
|
|
||||||
|
Consider dependencies between steps. Steps can run in parallel if they don't depend on each other.
|
||||||
|
|
||||||
|
Output ONLY a JSON array, no other text."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
task_svc: TaskService = None,
|
||||||
|
llm_client = None # LLM client for plan generation
|
||||||
|
):
|
||||||
|
self.task_service = task_svc or task_service
|
||||||
|
self.llm = llm_client
|
||||||
|
|
||||||
|
async def generate_plan(
|
||||||
|
self,
|
||||||
|
goal: str,
|
||||||
|
context: Dict[str, Any] = None
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate a plan from goal using LLM"""
|
||||||
|
if not self.llm:
|
||||||
|
logger.warning("No LLM client configured for planning")
|
||||||
|
return []
|
||||||
|
|
||||||
|
user_msg = f"Goal: {goal}\n\n"
|
||||||
|
if context:
|
||||||
|
if context.get("available_tools"):
|
||||||
|
user_msg += f"Available tools: {', '.join(context['available_tools'])}\n"
|
||||||
|
if context.get("constraints"):
|
||||||
|
user_msg += f"Constraints: {context['constraints']}\n"
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": self.PLANNER_SYSTEM},
|
||||||
|
{"role": "user", "content": user_msg}
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.llm.sync_call(messages=messages)
|
||||||
|
plan_text = response.get("content", "")
|
||||||
|
|
||||||
|
# Extract JSON
|
||||||
|
start = plan_text.find("[")
|
||||||
|
end = plan_text.rfind("]") + 1
|
||||||
|
if start != -1 and end > start:
|
||||||
|
plan_json = plan_text[start:end]
|
||||||
|
steps = json.loads(plan_json)
|
||||||
|
|
||||||
|
# Normalize steps
|
||||||
|
normalized = []
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
normalized.append({
|
||||||
|
"name": step.get("name", f"Step {i+1}"),
|
||||||
|
"description": step.get("description", ""),
|
||||||
|
"goal": step.get("goal", step.get("description", "")),
|
||||||
|
"depends_on": step.get("depends_on", [])
|
||||||
|
})
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Plan generation failed: {e}")
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def create_task_with_plan(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
goal: str,
|
||||||
|
description: str = "",
|
||||||
|
auto_plan: bool = True,
|
||||||
|
context: Dict[str, Any] = None
|
||||||
|
) -> Task:
|
||||||
|
"""Create a task and optionally auto-generate plan"""
|
||||||
|
task = self.task_service.create_task(
|
||||||
|
name=name,
|
||||||
|
goal=goal,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
|
||||||
|
if auto_plan:
|
||||||
|
# Generate plan using LLM
|
||||||
|
steps = await self.generate_plan(goal, context)
|
||||||
|
if steps:
|
||||||
|
self.task_service.set_steps_from_plan(task.id, steps)
|
||||||
|
logger.info(f"Generated {len(steps)} steps for task {task.id}")
|
||||||
|
else:
|
||||||
|
# Fallback: single step with the goal
|
||||||
|
self.task_service.add_steps(task.id, [{
|
||||||
|
"name": "Execute Goal",
|
||||||
|
"description": description,
|
||||||
|
"goal": goal
|
||||||
|
}])
|
||||||
|
|
||||||
|
return self.task_service.get_task(task.id)
|
||||||
|
|
||||||
|
def get_execution_plan(self, task_id: str) -> Dict[str, Any]:
|
||||||
|
"""Get execution plan for a task"""
|
||||||
|
task = self.task_service.get_task(task_id)
|
||||||
|
if not task:
|
||||||
|
return {"error": "Task not found"}
|
||||||
|
|
||||||
|
graph = self.task_service.build_graph(task_id)
|
||||||
|
if not graph:
|
||||||
|
return {"error": "Failed to build graph"}
|
||||||
|
|
||||||
|
sorted_steps = graph.topological_sort()
|
||||||
|
parallel_levels = graph.get_parallel_levels()
|
||||||
|
ready_steps = graph.get_ready_steps()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"task_name": task.name,
|
||||||
|
"goal": task.goal,
|
||||||
|
"total_steps": len(task.steps),
|
||||||
|
"completed_steps": len(task.get_completed_step_ids()),
|
||||||
|
"execution_order": [s.id for s in sorted_steps],
|
||||||
|
"parallel_levels": [[s.id for s in level] for level in parallel_levels],
|
||||||
|
"ready_steps": [s.id for s in ready_steps],
|
||||||
|
"can_execute": len(ready_steps) > 0,
|
||||||
|
"steps": [s.to_dict() for s in task.steps]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AgentWithPlan:
|
||||||
|
"""Agent execution with built-in task planning"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
chat_service,
|
||||||
|
task_executor: TaskExecutorService = None
|
||||||
|
):
|
||||||
|
self.chat_service = chat_service
|
||||||
|
self.task_executor = task_executor or TaskExecutorService()
|
||||||
|
|
||||||
|
async def execute_with_plan(
|
||||||
|
self,
|
||||||
|
goal: str,
|
||||||
|
context: Dict[str, Any]
|
||||||
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||||
|
"""Execute goal with automatic planning"""
|
||||||
|
# 1. Create task and generate plan
|
||||||
|
yield sse_event("planning", {"status": "generating", "goal": goal})
|
||||||
|
|
||||||
|
task = await self.task_executor.create_task_with_plan(
|
||||||
|
name=f"Task: {goal[:50]}...",
|
||||||
|
goal=goal,
|
||||||
|
context=context
|
||||||
|
)
|
||||||
|
|
||||||
|
if not task.steps:
|
||||||
|
yield sse_event("error", {"content": "Failed to generate plan"})
|
||||||
|
return
|
||||||
|
|
||||||
|
yield sse_event("plan_created", {
|
||||||
|
"task_id": task.id,
|
||||||
|
"steps": [s.to_dict() for s in task.steps]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. Execute steps
|
||||||
|
graph = self.task_executor.task_service.build_graph(task.id)
|
||||||
|
if not graph:
|
||||||
|
yield sse_event("error", {"content": "Failed to build execution graph"})
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get execution order
|
||||||
|
sorted_steps = graph.topological_sort()
|
||||||
|
completed_ids = []
|
||||||
|
|
||||||
|
for step in sorted_steps:
|
||||||
|
yield sse_event("step_start", {
|
||||||
|
"task_id": task.id,
|
||||||
|
"step_id": step.id,
|
||||||
|
"step_name": step.name,
|
||||||
|
"step_goal": step.goal,
|
||||||
|
"progress": len(completed_ids) + 1,
|
||||||
|
"total": len(sorted_steps)
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. Execute step with Agent
|
||||||
|
# For now, execute as a chat message
|
||||||
|
step_messages = [
|
||||||
|
{"role": "system", "content": f"You are executing step: {step.name}\n\n{step.goal}"},
|
||||||
|
{"role": "user", "content": f"Execute: {step.goal}"}
|
||||||
|
]
|
||||||
|
|
||||||
|
step_result = {"messages": [], "tool_calls": []}
|
||||||
|
|
||||||
|
# Stream response from agent
|
||||||
|
async for event in self._stream_step(step_messages, context):
|
||||||
|
if event.startswith("data: "):
|
||||||
|
data = event[6:]
|
||||||
|
yield event
|
||||||
|
# Collect results
|
||||||
|
if "step_complete" in data or "done" in data:
|
||||||
|
step_result["completed"] = True
|
||||||
|
|
||||||
|
# Mark step complete
|
||||||
|
self.task_executor.task_service.update_step_status(
|
||||||
|
task.id, step.id, StepStatus.COMPLETED,
|
||||||
|
{"result": step_result}
|
||||||
|
)
|
||||||
|
completed_ids.append(step.id)
|
||||||
|
|
||||||
|
yield sse_event("step_complete", {
|
||||||
|
"task_id": task.id,
|
||||||
|
"step_id": step.id,
|
||||||
|
"progress": len(completed_ids),
|
||||||
|
"total": len(sorted_steps)
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. Task complete
|
||||||
|
self.task_executor.task_service.update_task_status(
|
||||||
|
task.id, TaskStatus.COMPLETED
|
||||||
|
)
|
||||||
|
|
||||||
|
yield sse_event("task_complete", {
|
||||||
|
"task_id": task.id,
|
||||||
|
"completed_steps": len(completed_ids)
|
||||||
|
})
|
||||||
|
|
||||||
|
async def _stream_step(
|
||||||
|
self,
|
||||||
|
messages: List[Dict],
|
||||||
|
context: Dict[str, Any]
|
||||||
|
) -> AsyncGenerator[str, None]:
|
||||||
|
"""Stream response for a step (placeholder - integrate with ChatService)"""
|
||||||
|
# TODO: Integrate with actual ChatService streaming
|
||||||
|
yield sse_event("info", {"content": "Step execution not yet integrated"})
|
||||||
|
|
||||||
|
|
||||||
|
# Factory function
|
||||||
|
def create_task_executor(llm_client=None) -> TaskExecutorService:
|
||||||
|
"""Create TaskExecutorService with optional LLM client"""
|
||||||
|
return TaskExecutorService(llm_client=llm_client)
|
||||||
Loading…
Reference in New Issue