feat(service): implement bizdev service with independent task drivers
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
d54ba6ea77
commit
77c17b7bc0
@@ -0,0 +1,151 @@
|
||||
package bizdev
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/agent"
|
||||
"chat-agent-system/internal/config"
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/quote"
|
||||
"chat-agent-system/internal/template"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
cfg *config.Config
|
||||
kb *kb.Client
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Config) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
kb: kb.NewClient(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
var SupportedTasks = []TaskMeta{
|
||||
{Key: "intake", Title: "用户初始需求收集", ArtifactTitle: "用户初始需求收集表"},
|
||||
{Key: "research", Title: "调研", ArtifactTitle: "调研报告"},
|
||||
{Key: "feasibility_dev", Title: "设计开发可行性评估", ArtifactTitle: "设计开发可行性评估报告"},
|
||||
{Key: "feasibility_reg", Title: "注册路径可行性评估", ArtifactTitle: "注册路径可行性评估报告"},
|
||||
{Key: "user_requirements", Title: "用户需求表", ArtifactTitle: "用户需求表"},
|
||||
{Key: "prelim_design", Title: "系统初步设计方案", ArtifactTitle: "系统初步设计方案"},
|
||||
{Key: "cdo_quote", Title: "CDO报价单", ArtifactTitle: "CDO报价单(含定价核算 Excel)"},
|
||||
}
|
||||
|
||||
func (s *Service) ListTasks() []TaskMeta {
|
||||
return SupportedTasks
|
||||
}
|
||||
|
||||
func taskArtifactTitle(taskKey string) string {
|
||||
for _, t := range SupportedTasks {
|
||||
if t.Key == taskKey {
|
||||
return t.ArtifactTitle
|
||||
}
|
||||
}
|
||||
return taskKey
|
||||
}
|
||||
|
||||
func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRunResult, error) {
|
||||
start := time.Now()
|
||||
|
||||
workDir := req.WorkDir
|
||||
if workDir == "" {
|
||||
ts := time.Now().UnixNano()
|
||||
workDir = filepath.Join(s.cfg.DataDir, "workspaces", fmt.Sprintf("task_%s_%d", req.TaskKey, ts))
|
||||
}
|
||||
if err := os.MkdirAll(workDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create workdir: %w", err)
|
||||
}
|
||||
|
||||
tmpl, err := template.GetDocTemplate(req.TaskKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vars := map[string]string{
|
||||
"project_name": req.ProjectName,
|
||||
"customer_name": req.CustomerName,
|
||||
"device_type": req.DeviceType,
|
||||
"intended_use": req.IntendedUse,
|
||||
"target_market": req.TargetMarket,
|
||||
"generated_at": time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
for k, v := range req.Fields {
|
||||
vars[k] = v
|
||||
}
|
||||
renderedTmpl := template.RenderDoc(tmpl, vars)
|
||||
_ = os.WriteFile(filepath.Join(workDir, "template.md"), []byte(renderedTmpl), 0o644)
|
||||
|
||||
inst, _ := template.GetAgentInstruction("writer")
|
||||
_ = os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(inst), 0o644)
|
||||
taskPrompt := fmt.Sprintf("# 任务执行: %s\n\n请按 template.md 的指导撰写正文并交付到 output.md。\n", req.TaskKey)
|
||||
_ = os.WriteFile(filepath.Join(workDir, "TASK.md"), []byte(taskPrompt), 0o644)
|
||||
|
||||
backendName := req.Backend
|
||||
if backendName == "" {
|
||||
backendName = s.cfg.AgentBackend
|
||||
}
|
||||
backend := agent.NewBackend(backendName, s.cfg)
|
||||
|
||||
timeout := req.Timeout
|
||||
if timeout == 0 {
|
||||
timeout = s.cfg.AgentTimeout
|
||||
}
|
||||
|
||||
backendRes, err := backend.Execute(ctx, agent.Request{
|
||||
WorkDir: workDir,
|
||||
Prompt: taskPrompt,
|
||||
Instructions: inst,
|
||||
Targets: []string{"output.md"},
|
||||
Timeout: timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("backend execute: %w", err)
|
||||
}
|
||||
|
||||
markdownBody := renderedTmpl
|
||||
if backendRes != nil && len(backendRes.Patches["output.md"]) > 0 && backendName != "stub" {
|
||||
markdownBody = string(backendRes.Patches["output.md"])
|
||||
}
|
||||
|
||||
var attachments []Attachment
|
||||
if req.TaskKey == "cdo_quote" {
|
||||
xlsxPath := filepath.Join(workDir, "CDO报价核算-V01.xlsx")
|
||||
in := quote.Input{
|
||||
CustomerName: req.CustomerName,
|
||||
ProductName: req.ProjectName,
|
||||
Hours: map[string]float64{
|
||||
"结构工程师": 320,
|
||||
"硬件工程师": 320,
|
||||
"软件工程师": 480,
|
||||
"项目经理": 160,
|
||||
},
|
||||
}
|
||||
if _, err := quote.CalculateAndFill(in, "templates/xlsx/cdo_quote.xlsx", xlsxPath); err != nil {
|
||||
return nil, fmt.Errorf("cdo quote: %w", err)
|
||||
}
|
||||
data, err := os.ReadFile(xlsxPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read cdo quote xlsx: %w", err)
|
||||
}
|
||||
attachments = append(attachments, Attachment{
|
||||
Filename: "CDO报价核算-V01.xlsx",
|
||||
ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
return &TaskRunResult{
|
||||
TaskKey: req.TaskKey,
|
||||
ArtifactTitle: taskArtifactTitle(req.TaskKey),
|
||||
MarkdownBody: markdownBody,
|
||||
Attachments: attachments,
|
||||
WorkDir: workDir,
|
||||
BackendUsed: backendName,
|
||||
Duration: time.Since(start),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package bizdev_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/bizdev"
|
||||
"chat-agent-system/internal/config"
|
||||
)
|
||||
|
||||
func TestExecuteTaskIndependent(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
DataDir: tmpDir,
|
||||
AgentBackend: "stub",
|
||||
AgentTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
svc := bizdev.NewService(cfg)
|
||||
|
||||
// 测试 1: 单独执行 intake (不依赖任何前序)
|
||||
intakeRes, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{
|
||||
TaskKey: "intake",
|
||||
ProjectName: "微创吻合器研发",
|
||||
CustomerName: "北京天工",
|
||||
DeviceType: "腔镜微创器械",
|
||||
IntendedUse: "微创手术软组织缝合切除",
|
||||
TargetMarket: "中国 (NMPA)",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute intake failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(intakeRes.MarkdownBody, "用户初始需求收集表") {
|
||||
t.Errorf("intake response missing title")
|
||||
}
|
||||
|
||||
// 测试 2: 单独执行 cdo_quote (直接输出 Excel 附件)
|
||||
quoteRes, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{
|
||||
TaskKey: "cdo_quote",
|
||||
ProjectName: "微创吻合器研发",
|
||||
CustomerName: "北京天工",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute cdo_quote failed: %v", err)
|
||||
}
|
||||
if len(quoteRes.Attachments) == 0 {
|
||||
t.Fatalf("cdo_quote must output Excel attachment")
|
||||
}
|
||||
if quoteRes.Attachments[0].Filename != "CDO报价核算-V01.xlsx" {
|
||||
t.Errorf("unexpected attachment filename: %s", quoteRes.Attachments[0].Filename)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package bizdev
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TaskMeta struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
ArtifactTitle string `json:"artifact_title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type MaterialInput struct {
|
||||
Filename string `json:"filename"`
|
||||
Content []byte `json:"content"`
|
||||
}
|
||||
|
||||
type TaskRunRequest struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
ProjectName string `json:"project_name"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
IntendedUse string `json:"intended_use"`
|
||||
TargetMarket string `json:"target_market"`
|
||||
Fields map[string]string `json:"fields"`
|
||||
Materials []MaterialInput `json:"materials"`
|
||||
Backend string `json:"backend"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Filename string `json:"filename"`
|
||||
ContentType string `json:"content_type"`
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
type TaskRunResult struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
ArtifactTitle string `json:"artifact_title"`
|
||||
MarkdownBody string `json:"markdown_body"`
|
||||
ExtractedData map[string]string `json:"extracted_data"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
BackendUsed string `json:"backend_used"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
}
|
||||
|
||||
type BizdevService interface {
|
||||
ListTasks() []TaskMeta
|
||||
ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRunResult, error)
|
||||
}
|
||||
Reference in New Issue
Block a user