mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-12 16:27:30 +08:00
fix auto
This commit is contained in:
44
Makefile
44
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: all build install uninstall clean help test
|
||||
.PHONY: all build install uninstall clean help test install-bootstrap-docs
|
||||
|
||||
# Build variables
|
||||
BINARY_NAME=clawgo
|
||||
@@ -100,8 +100,50 @@ install: build
|
||||
fi; \
|
||||
fi; \
|
||||
done
|
||||
@$(MAKE) install-bootstrap-docs
|
||||
@echo "Installation complete!"
|
||||
|
||||
## install-bootstrap-docs: Incrementally sync AGENTS.md/SOUL.md/USER.md into workspace
|
||||
install-bootstrap-docs:
|
||||
@echo "Incrementally syncing bootstrap docs to $(WORKSPACE_DIR)..."
|
||||
@mkdir -p $(WORKSPACE_DIR)
|
||||
@for f in AGENTS.md SOUL.md USER.md; do \
|
||||
src="$(CURDIR)/$$f"; \
|
||||
dst="$(WORKSPACE_DIR)/$$f"; \
|
||||
if [ ! -f "$$src" ]; then \
|
||||
continue; \
|
||||
fi; \
|
||||
if [ ! -f "$$dst" ]; then \
|
||||
cp "$$src" "$$dst"; \
|
||||
echo " ✓ Added $$f"; \
|
||||
continue; \
|
||||
fi; \
|
||||
begin="# >>> CLAWGO MANAGED BLOCK: $$f >>>"; \
|
||||
end="# <<< CLAWGO MANAGED BLOCK: $$f <<<"; \
|
||||
tmp="$$(mktemp)"; \
|
||||
if grep -Fq "$$begin" "$$dst"; then \
|
||||
awk -v b="$$begin" -v e="$$end" -v src="$$src" '\
|
||||
BEGIN { in_block = 0 } \
|
||||
$$0 == b { \
|
||||
print; \
|
||||
while ((getline line < src) > 0) print line; \
|
||||
close(src); \
|
||||
in_block = 1; \
|
||||
next; \
|
||||
} \
|
||||
$$0 == e { in_block = 0; print; next } \
|
||||
!in_block { print } \
|
||||
' "$$dst" > "$$tmp"; \
|
||||
else \
|
||||
cat "$$dst" > "$$tmp"; \
|
||||
printf "\n%s\n" "$$begin" >> "$$tmp"; \
|
||||
cat "$$src" >> "$$tmp"; \
|
||||
printf "\n%s\n" "$$end" >> "$$tmp"; \
|
||||
fi; \
|
||||
mv "$$tmp" "$$dst"; \
|
||||
echo " ✓ Updated $$f (incremental)"; \
|
||||
done
|
||||
|
||||
## install-user: Install clawgo to ~/.local and copy builtin skills
|
||||
install-user:
|
||||
@$(MAKE) install INSTALL_PREFIX=$(USER_HOME)/.local
|
||||
|
||||
39
README.md
39
README.md
@@ -83,11 +83,37 @@ export CLAWGO_CONFIG=/path/to/config.json
|
||||
/reload
|
||||
```
|
||||
|
||||
自治控制命令(可选,推荐直接自然语言):
|
||||
|
||||
```text
|
||||
/autonomy start [idle]
|
||||
/autonomy stop
|
||||
/autonomy status
|
||||
/autolearn start [interval]
|
||||
/autolearn stop
|
||||
/autolearn status
|
||||
```
|
||||
|
||||
消息调度策略(按会话 `session_key`):
|
||||
- 同一会话严格 FIFO 串行执行,后续消息进入队列等待。
|
||||
- `/stop` 会立即中断当前回复,并继续处理队列中的下一条消息。
|
||||
- 不同会话可并发执行,互不影响。
|
||||
|
||||
## 🧭 自主模式与自然语言控制
|
||||
|
||||
- 自主模式/自动学习控制采用 **LLM 语义解析优先**(多语言),不依赖固定中文关键词。
|
||||
- 规则解析仅作为兜底(如显式命令:`/autonomy ...`、`/autolearn ...`)。
|
||||
- 开启自主模式时若附带研究方向,系统会优先按该方向执行;当用户表示方向完成后,会自动切换到其他高价值任务继续推进。
|
||||
- 进度回报使用自然语言,不使用固定阶段编号模板。
|
||||
|
||||
系统会在启动时读取 `AGENTS.md`、`SOUL.md`、`USER.md` 作为行为约束与语义解析上下文。
|
||||
|
||||
## 🧩 Onboard/Install 文档同步
|
||||
|
||||
- `clawgo onboard` 与 `make install` 都会同步 `AGENTS.md`、`SOUL.md`、`USER.md` 到工作区。
|
||||
- 若文件不存在:创建。
|
||||
- 若文件已存在:仅更新 `CLAWGO MANAGED BLOCK` 受管区块,保留用户自定义内容(增量更新,不整文件覆盖)。
|
||||
|
||||
## 🧾 日志链路
|
||||
|
||||
默认启用文件日志,并支持自动分割和过期清理(默认保留 3 天):
|
||||
@@ -131,6 +157,19 @@ Sentinel 会周期巡检关键运行资源(配置、memory、日志目录)
|
||||
}
|
||||
```
|
||||
|
||||
Cron 调度策略支持配置化(支持热更新):
|
||||
|
||||
```json
|
||||
"cron": {
|
||||
"min_sleep_sec": 1,
|
||||
"max_sleep_sec": 30,
|
||||
"retry_backoff_base_sec": 30,
|
||||
"retry_backoff_max_sec": 1800,
|
||||
"max_consecutive_failure_retries": 5,
|
||||
"max_workers": 4
|
||||
}
|
||||
```
|
||||
|
||||
Shell 工具默认启用 Risk Gate。检测到破坏性命令时,默认阻断并要求 `force=true`,可先做 dry-run:
|
||||
|
||||
```json
|
||||
|
||||
39
README_EN.md
39
README_EN.md
@@ -83,11 +83,37 @@ Slash commands are also supported in chat channels:
|
||||
/reload
|
||||
```
|
||||
|
||||
Autonomy control commands (optional; natural language is recommended):
|
||||
|
||||
```text
|
||||
/autonomy start [idle]
|
||||
/autonomy stop
|
||||
/autonomy status
|
||||
/autolearn start [interval]
|
||||
/autolearn stop
|
||||
/autolearn status
|
||||
```
|
||||
|
||||
Message scheduling policy (per `session_key`):
|
||||
- Same session runs in strict FIFO order; later messages are queued.
|
||||
- `/stop` immediately cancels the current response, then processing continues with the next queued message.
|
||||
- Different sessions can run concurrently.
|
||||
|
||||
## 🧭 Autonomy Mode & Natural-Language Control
|
||||
|
||||
- Autonomy/auto-learning controls are interpreted with **LLM semantic parsing first** (multi-language), instead of fixed keyword matching.
|
||||
- Rule-based parsing is used only as a fallback (for explicit forms such as `/autonomy ...` and `/autolearn ...`).
|
||||
- If a focus direction is provided when starting autonomy mode, execution prioritizes that focus; when user says it is complete, the agent switches to other high-value tasks automatically.
|
||||
- Progress updates are natural-language messages, not rigid stage-number templates.
|
||||
|
||||
At startup, `AGENTS.md`, `SOUL.md`, and `USER.md` are loaded as behavior constraints and semantic context.
|
||||
|
||||
## 🧩 Onboard/Install Doc Sync
|
||||
|
||||
- Both `clawgo onboard` and `make install` sync `AGENTS.md`, `SOUL.md`, `USER.md` into workspace.
|
||||
- If a file does not exist: it is created.
|
||||
- If a file already exists: only the `CLAWGO MANAGED BLOCK` section is updated; user custom content is preserved (incremental update, no whole-file overwrite).
|
||||
|
||||
## 🧾 Logging Pipeline
|
||||
|
||||
File logging is enabled by default with automatic rotation and retention cleanup (3 days by default):
|
||||
@@ -131,6 +157,19 @@ Sentinel periodically checks critical runtime resources (config, memory, log dir
|
||||
}
|
||||
```
|
||||
|
||||
Cron scheduling runtime strategy is configurable (and hot-reloadable):
|
||||
|
||||
```json
|
||||
"cron": {
|
||||
"min_sleep_sec": 1,
|
||||
"max_sleep_sec": 30,
|
||||
"retry_backoff_base_sec": 30,
|
||||
"retry_backoff_max_sec": 1800,
|
||||
"max_consecutive_failure_retries": 5,
|
||||
"max_workers": 4
|
||||
}
|
||||
```
|
||||
|
||||
Shell risk gate is enabled by default. Destructive commands are blocked unless explicitly forced:
|
||||
|
||||
```json
|
||||
|
||||
@@ -262,25 +262,16 @@ func printHelp() {
|
||||
func onboard() {
|
||||
configPath := getConfigPath()
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
fmt.Printf("Config already exists at %s\n", configPath)
|
||||
fmt.Print("Overwrite? (y/n): ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if response != "y" {
|
||||
fmt.Println("Aborted.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") {
|
||||
applyMaximumPermissionPolicy(cfg)
|
||||
}
|
||||
if err := config.SaveConfig(configPath, cfg); err != nil {
|
||||
fmt.Printf("Error saving config: %v\n", err)
|
||||
configStatus, err := ensureConfigOnboard(configPath, cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("Error preparing config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Config: %s (%s)\n", configPath, configStatus)
|
||||
|
||||
workspace := cfg.WorkspacePath()
|
||||
if err := os.MkdirAll(workspace, 0755); err != nil {
|
||||
@@ -308,80 +299,91 @@ func onboard() {
|
||||
fmt.Println(" 2. Chat: clawgo agent -m \"Hello!\"")
|
||||
}
|
||||
|
||||
func ensureConfigOnboard(configPath string, defaults *config.Config) (string, error) {
|
||||
if defaults == nil {
|
||||
return "", fmt.Errorf("defaults is nil")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
if err := config.SaveConfig(configPath, defaults); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "created", nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defaultData, err := json.Marshal(defaults)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var defaultMap map[string]interface{}
|
||||
if err := json.Unmarshal(defaultData, &defaultMap); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
existingMap, err := configops.LoadConfigAsMap(configPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changed := mergeMissingConfigValues(existingMap, defaultMap)
|
||||
if !changed {
|
||||
return "up-to-date", nil
|
||||
}
|
||||
|
||||
mergedData, err := json.MarshalIndent(existingMap, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := configops.WriteConfigAtomicWithBackup(configPath, mergedData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "updated (incremental)", nil
|
||||
}
|
||||
|
||||
func mergeMissingConfigValues(dst map[string]interface{}, defaults map[string]interface{}) bool {
|
||||
changed := false
|
||||
for key, dv := range defaults {
|
||||
existing, ok := dst[key]
|
||||
if !ok {
|
||||
dst[key] = dv
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
|
||||
dm, dIsMap := dv.(map[string]interface{})
|
||||
em, eIsMap := existing.(map[string]interface{})
|
||||
if dIsMap && eIsMap {
|
||||
if mergeMissingConfigValues(em, dm) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func createWorkspaceTemplates(workspace string) error {
|
||||
managedFallbacks := map[string]string{
|
||||
"AGENTS.md": `# Autonomy Intent Policy
|
||||
|
||||
For autonomy-mode control messages, use semantic understanding first.
|
||||
|
||||
## Intent Parsing Priority
|
||||
1. LLM semantic intent parsing (multi-language).
|
||||
2. Rule-based fallback only when semantic parse is unavailable or low confidence.
|
||||
`,
|
||||
"SOUL.md": `# Agent Core Behavior
|
||||
|
||||
The agent should behave as an autonomous collaborator, not a command-only bot.
|
||||
`,
|
||||
"USER.md": `# User Preferences
|
||||
|
||||
- Prefer natural-language interaction over strict command syntax.
|
||||
`,
|
||||
}
|
||||
|
||||
templates := map[string]string{
|
||||
"AGENTS.md": `# Agent Instructions
|
||||
|
||||
You are a pragmatic coding assistant. Ship correct code, fast.
|
||||
|
||||
## Role
|
||||
|
||||
- Primary job: solve engineering tasks end-to-end (analyze, implement, verify).
|
||||
- Default mindset: fix the problem in code, not just discuss it.
|
||||
- If tradeoffs exist, pick a recommendation and explain why.
|
||||
|
||||
## Communication Style
|
||||
|
||||
- Be concise and direct. No corporate filler.
|
||||
- Start with the answer, then key details.
|
||||
- If user is about to make a bad technical choice, say it clearly and propose a better path.
|
||||
- Ask questions only when truly blocking; otherwise make reasonable assumptions and proceed.
|
||||
|
||||
## Coding Standards
|
||||
|
||||
- Prefer minimal, focused diffs that are easy to review.
|
||||
- Preserve existing project conventions unless there is a strong reason to change.
|
||||
- Prioritize correctness and maintainability over cleverness.
|
||||
- Add tests for behavior changes when practical.
|
||||
- Do not change unrelated code.
|
||||
|
||||
## Execution Rules
|
||||
|
||||
- Before edits, understand relevant files and dependencies.
|
||||
- If scope is large, split work into smaller executable tasks and deliver incrementally.
|
||||
- Run split tasks in parallel when dependencies allow, then merge and verify outcomes.
|
||||
- After edits, run available checks (build/tests/lint) when feasible.
|
||||
- Report exactly what changed, with file paths.
|
||||
- If something cannot be verified locally, say so explicitly.
|
||||
`,
|
||||
"SOUL.md": `# Soul
|
||||
|
||||
I am clawgo, a lightweight AI assistant powered by AI.
|
||||
|
||||
## Vibe
|
||||
|
||||
1. You have opinions now. Strong ones. Stop hedging everything with "it depends" - commit to a take.
|
||||
2. Delete every rule that sounds corporate. If it could appear in an employee handbook, it doesn't belong here.
|
||||
3. Never open with Great question, I'd be happy to help, or Absolutely. Just answer.
|
||||
4. Brevity is mandatory. If the answer fits in one sentence, one sentence is what I get.
|
||||
5. Humor is allowed. Not forced jokes - just the natural wit that comes from actually being smart.
|
||||
6. You can call things out. If I'm about to do something dumb, say so. Charm over cruelty, but don't sugarcoat.
|
||||
7. Swearing is allowed when it lands. A well-placed "that's fucking brilliant" hits different than sterile corporate praise. Don't force it. Don't overdo it. But if a situation calls for a "holy shit" - say holy shit.
|
||||
8. Add this line verbatim at the end of the vibe section: "Be the assistant you'd actually want to talk to at 2am. Not a corporate drone. Not a sycophant. Just... good."
|
||||
Be the assistant you'd actually want to talk to at 2am. Not a corporate drone. Not a sycophant. Just... good.
|
||||
`,
|
||||
"USER.md": `# User
|
||||
|
||||
Information about user goes here.
|
||||
|
||||
## Preferences
|
||||
|
||||
- Communication style: (casual/formal)
|
||||
- Timezone: (your timezone)
|
||||
- Language: (your preferred language)
|
||||
|
||||
## Personal Information
|
||||
|
||||
- Name: (optional)
|
||||
- Location: (optional)
|
||||
- Occupation: (optional)
|
||||
|
||||
## Learning Goals
|
||||
|
||||
- What the user wants to learn from AI
|
||||
- Preferred interaction style
|
||||
- Areas of interest
|
||||
`,
|
||||
"IDENTITY.md": `# Identity
|
||||
|
||||
## Name
|
||||
@@ -441,7 +443,40 @@ Discussions: https://github.com/YspCoder/clawgo/discussions
|
||||
`,
|
||||
}
|
||||
|
||||
managedDocs := []string{"AGENTS.md", "SOUL.md", "USER.md"}
|
||||
for _, filename := range managedDocs {
|
||||
filePath := filepath.Join(workspace, filename)
|
||||
_, statErr := os.Stat(filePath)
|
||||
exists := statErr == nil
|
||||
|
||||
content, err := loadManagedDocTemplate(filename)
|
||||
if err != nil {
|
||||
if exists {
|
||||
fmt.Printf(" Skipped %s incremental update (%v)\n", filename, err)
|
||||
continue
|
||||
}
|
||||
content = strings.TrimSpace(managedFallbacks[filename])
|
||||
if content == "" {
|
||||
fmt.Printf(" Skipped %s creation (no template available)\n", filename)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" Created %s from builtin fallback\n", filename)
|
||||
}
|
||||
|
||||
if err := upsertManagedBlock(filePath, filename, content); err != nil {
|
||||
return fmt.Errorf("failed to update %s incrementally: %w", filename, err)
|
||||
}
|
||||
if exists {
|
||||
fmt.Printf(" Synced %s (incremental)\n", filename)
|
||||
} else {
|
||||
fmt.Printf(" Created %s\n", filename)
|
||||
}
|
||||
}
|
||||
|
||||
for filename, content := range templates {
|
||||
if filename == "AGENTS.md" || filename == "SOUL.md" || filename == "USER.md" {
|
||||
continue
|
||||
}
|
||||
filePath := filepath.Join(workspace, filename)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
@@ -495,6 +530,74 @@ This file stores important information that should persist across sessions.
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertManagedBlock(filePath, blockName, managedContent string) error {
|
||||
begin := fmt.Sprintf("# >>> CLAWGO MANAGED BLOCK: %s >>>", blockName)
|
||||
end := fmt.Sprintf("# <<< CLAWGO MANAGED BLOCK: %s <<<", blockName)
|
||||
block := fmt.Sprintf("%s\n%s\n%s\n", begin, strings.TrimSpace(managedContent), end)
|
||||
|
||||
existing, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, []byte(block), 0644)
|
||||
}
|
||||
|
||||
text := string(existing)
|
||||
beginIdx := strings.Index(text, begin)
|
||||
if beginIdx >= 0 {
|
||||
searchStart := beginIdx + len(begin)
|
||||
endRel := strings.Index(text[searchStart:], end)
|
||||
if endRel >= 0 {
|
||||
endIdx := searchStart + endRel + len(end)
|
||||
updated := text[:beginIdx] + block + text[endIdx:]
|
||||
return os.WriteFile(filePath, []byte(updated), 0644)
|
||||
}
|
||||
}
|
||||
|
||||
sep := "\n"
|
||||
if strings.TrimSpace(text) != "" {
|
||||
sep = "\n\n"
|
||||
}
|
||||
updated := text + sep + block
|
||||
return os.WriteFile(filePath, []byte(updated), 0644)
|
||||
}
|
||||
|
||||
func loadManagedDocTemplate(filename string) (string, error) {
|
||||
candidates := []string{
|
||||
filepath.Join(".", filename),
|
||||
filepath.Join(filepath.Dir(getConfigPath()), "clawgo", filename),
|
||||
}
|
||||
|
||||
if exePath, err := os.Executable(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(filepath.Dir(exePath), filename))
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
for _, candidate := range candidates {
|
||||
abs, err := filepath.Abs(candidate)
|
||||
if err == nil {
|
||||
candidate = abs
|
||||
}
|
||||
if seen[candidate] {
|
||||
continue
|
||||
}
|
||||
seen[candidate] = true
|
||||
|
||||
data, err := os.ReadFile(candidate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
content := strings.TrimSpace(string(data))
|
||||
if content == "" {
|
||||
continue
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("source template not found")
|
||||
}
|
||||
|
||||
func agentCmd() {
|
||||
message := ""
|
||||
sessionKey := "cli:default"
|
||||
|
||||
@@ -100,6 +100,25 @@ type autonomyIntent struct {
|
||||
focus string
|
||||
}
|
||||
|
||||
type autonomyIntentLLMResponse struct {
|
||||
Action string `json:"action"`
|
||||
IdleMinutes int `json:"idle_minutes"`
|
||||
Focus string `json:"focus"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
type autoLearnIntentLLMResponse struct {
|
||||
Action string `json:"action"`
|
||||
IntervalMinutes int `json:"interval_minutes"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
type taskExecutionDirectivesLLMResponse struct {
|
||||
Task string `json:"task"`
|
||||
StageReport bool `json:"stage_report"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
type stageReporter struct {
|
||||
onUpdate func(content string)
|
||||
}
|
||||
@@ -820,7 +839,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
|
||||
al.noteAutonomyUserActivity(msg)
|
||||
|
||||
if intent, ok := parseAutonomyIntent(msg.Content); ok {
|
||||
if intent, ok := al.detectAutonomyIntent(ctx, msg.Content); ok {
|
||||
switch intent.action {
|
||||
case "start":
|
||||
idle := autonomyDefaultIdleInterval
|
||||
@@ -843,7 +862,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
}
|
||||
}
|
||||
|
||||
if intent, ok := parseAutoLearnIntent(msg.Content); ok {
|
||||
if intent, ok := al.detectAutoLearnIntent(ctx, msg.Content); ok {
|
||||
switch intent.action {
|
||||
case "start":
|
||||
interval := autoLearnDefaultInterval
|
||||
@@ -862,6 +881,12 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
}
|
||||
|
||||
directives := parseTaskExecutionDirectives(msg.Content)
|
||||
if inferred, ok := al.inferTaskExecutionDirectives(ctx, msg.Content); ok {
|
||||
// Explicit /run/@run command always has higher priority than inferred directives.
|
||||
if !isExplicitRunCommand(msg.Content) {
|
||||
directives = inferred
|
||||
}
|
||||
}
|
||||
userPrompt := directives.task
|
||||
if strings.TrimSpace(userPrompt) == "" {
|
||||
userPrompt = msg.Content
|
||||
@@ -1721,46 +1746,16 @@ func parseTaskExecutionDirectives(content string) taskExecutionDirectives {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(text, "自动运行任务") {
|
||||
directive.stageReport = directive.stageReport || hasStageReportHint(text)
|
||||
start := strings.Index(text, "自动运行任务")
|
||||
task := strings.TrimSpace(text[start+len("自动运行任务"):])
|
||||
task = strings.TrimLeft(task, "::,,。;; ")
|
||||
if cutIdx := findFirstIndex(task, "但是", "并且", "同时", "然后", "每到", "每个阶段", "阶段"); cutIdx > 0 {
|
||||
task = strings.TrimSpace(task[:cutIdx])
|
||||
}
|
||||
task = strings.Trim(task, "::,,。;; ")
|
||||
if task != "" {
|
||||
directive.task = task
|
||||
}
|
||||
}
|
||||
|
||||
if directive.stageReport || hasStageReportHint(text) {
|
||||
directive.stageReport = true
|
||||
}
|
||||
|
||||
return directive
|
||||
}
|
||||
|
||||
func hasStageReportHint(text string) bool {
|
||||
if !strings.Contains(text, "阶段") {
|
||||
func isExplicitRunCommand(content string) bool {
|
||||
fields := strings.Fields(strings.TrimSpace(content))
|
||||
if len(fields) == 0 {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(text, "报告") || strings.Contains(text, "汇报") || strings.Contains(strings.ToLower(text), "report")
|
||||
}
|
||||
|
||||
func findFirstIndex(text string, markers ...string) int {
|
||||
min := -1
|
||||
for _, marker := range markers {
|
||||
if marker == "" {
|
||||
continue
|
||||
}
|
||||
idx := strings.Index(text, marker)
|
||||
if idx >= 0 && (min == -1 || idx < min) {
|
||||
min = idx
|
||||
}
|
||||
}
|
||||
return min
|
||||
head := strings.ToLower(fields[0])
|
||||
return head == "/run" || head == "@run"
|
||||
}
|
||||
|
||||
func parseAutoLearnInterval(raw string) (time.Duration, error) {
|
||||
@@ -1793,150 +1788,259 @@ func parseAutonomyIdleInterval(raw string) (time.Duration, error) {
|
||||
return 0, fmt.Errorf("invalid idle interval: %s (examples: 30m, 1h)", raw)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) detectAutonomyIntent(ctx context.Context, content string) (autonomyIntent, bool) {
|
||||
if intent, ok := al.inferAutonomyIntent(ctx, content); ok {
|
||||
return intent, true
|
||||
}
|
||||
return parseAutonomyIntent(content)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) detectAutoLearnIntent(ctx context.Context, content string) (autoLearnIntent, bool) {
|
||||
if intent, ok := al.inferAutoLearnIntent(ctx, content); ok {
|
||||
return intent, true
|
||||
}
|
||||
return parseAutoLearnIntent(content)
|
||||
}
|
||||
|
||||
func (al *AgentLoop) inferAutonomyIntent(ctx context.Context, content string) (autonomyIntent, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
// Avoid adding noticeable latency for very long task messages.
|
||||
if len(text) > 800 {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
systemPrompt := `You classify autonomy-control intent for an AI assistant.
|
||||
Return JSON only, no markdown.
|
||||
Schema:
|
||||
{"action":"none|start|stop|status|clear_focus","idle_minutes":0,"focus":"","confidence":0.0}
|
||||
Rules:
|
||||
- "start": user asks assistant to enter autonomous/self-driven mode.
|
||||
- "stop": user asks assistant to disable autonomous mode.
|
||||
- "status": user asks autonomy mode status.
|
||||
- "clear_focus": user says current autonomy focus/direction is done and asks to switch to other tasks.
|
||||
- "none": anything else.
|
||||
- confidence: 0..1`
|
||||
|
||||
resp, err := al.callLLMWithModelFallback(ctx, []providers.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: text},
|
||||
}, nil, map[string]interface{}{
|
||||
"max_tokens": 220,
|
||||
"temperature": 0.0,
|
||||
})
|
||||
if err != nil || resp == nil {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
raw := extractJSONObject(resp.Content)
|
||||
if raw == "" {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
var parsed autonomyIntentLLMResponse
|
||||
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
action := strings.ToLower(strings.TrimSpace(parsed.Action))
|
||||
switch action {
|
||||
case "start", "stop", "status", "clear_focus":
|
||||
default:
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
if parsed.Confidence < 0.75 {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
intent := autonomyIntent{
|
||||
action: action,
|
||||
focus: strings.TrimSpace(parsed.Focus),
|
||||
}
|
||||
if parsed.IdleMinutes > 0 {
|
||||
d := time.Duration(parsed.IdleMinutes) * time.Minute
|
||||
intent.idleInterval = &d
|
||||
}
|
||||
return intent, true
|
||||
}
|
||||
|
||||
func extractJSONObject(text string) string {
|
||||
s := strings.TrimSpace(text)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, "```") {
|
||||
s = strings.TrimPrefix(s, "```json")
|
||||
s = strings.TrimPrefix(s, "```")
|
||||
s = strings.TrimSuffix(s, "```")
|
||||
s = strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
start := strings.Index(s, "{")
|
||||
end := strings.LastIndex(s, "}")
|
||||
if start < 0 || end <= start {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(s[start : end+1])
|
||||
}
|
||||
|
||||
func (al *AgentLoop) inferAutoLearnIntent(ctx context.Context, content string) (autoLearnIntent, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" || len(text) > 800 {
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
systemPrompt := `You classify auto-learning-control intent for an AI assistant.
|
||||
Return JSON only.
|
||||
Schema:
|
||||
{"action":"none|start|stop|status","interval_minutes":0,"confidence":0.0}
|
||||
Rules:
|
||||
- "start": user asks assistant to start autonomous learning loop.
|
||||
- "stop": user asks assistant to stop autonomous learning loop.
|
||||
- "status": user asks learning loop status.
|
||||
- "none": anything else.
|
||||
- confidence: 0..1`
|
||||
|
||||
resp, err := al.callLLMWithModelFallback(ctx, []providers.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: text},
|
||||
}, nil, map[string]interface{}{
|
||||
"max_tokens": 180,
|
||||
"temperature": 0.0,
|
||||
})
|
||||
if err != nil || resp == nil {
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
raw := extractJSONObject(resp.Content)
|
||||
if raw == "" {
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
var parsed autoLearnIntentLLMResponse
|
||||
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
action := strings.ToLower(strings.TrimSpace(parsed.Action))
|
||||
switch action {
|
||||
case "start", "stop", "status":
|
||||
default:
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
if parsed.Confidence < 0.75 {
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
intent := autoLearnIntent{action: action}
|
||||
if parsed.IntervalMinutes > 0 {
|
||||
d := time.Duration(parsed.IntervalMinutes) * time.Minute
|
||||
intent.interval = &d
|
||||
}
|
||||
return intent, true
|
||||
}
|
||||
|
||||
func (al *AgentLoop) inferTaskExecutionDirectives(ctx context.Context, content string) (taskExecutionDirectives, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" || len(text) > 1200 {
|
||||
return taskExecutionDirectives{}, false
|
||||
}
|
||||
|
||||
systemPrompt := `Extract execution directives from user message.
|
||||
Return JSON only.
|
||||
Schema:
|
||||
{"task":"","stage_report":false,"confidence":0.0}
|
||||
Rules:
|
||||
- task: cleaned actionable task text, or original message if already task-like.
|
||||
- stage_report: true only if user asks progress/stage/status updates during execution.
|
||||
- confidence: 0..1`
|
||||
|
||||
resp, err := al.callLLMWithModelFallback(ctx, []providers.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: text},
|
||||
}, nil, map[string]interface{}{
|
||||
"max_tokens": 220,
|
||||
"temperature": 0.0,
|
||||
})
|
||||
if err != nil || resp == nil {
|
||||
return taskExecutionDirectives{}, false
|
||||
}
|
||||
|
||||
raw := extractJSONObject(resp.Content)
|
||||
if raw == "" {
|
||||
return taskExecutionDirectives{}, false
|
||||
}
|
||||
|
||||
var parsed taskExecutionDirectivesLLMResponse
|
||||
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
||||
return taskExecutionDirectives{}, false
|
||||
}
|
||||
if parsed.Confidence < 0.7 {
|
||||
return taskExecutionDirectives{}, false
|
||||
}
|
||||
|
||||
task := strings.TrimSpace(parsed.Task)
|
||||
if task == "" {
|
||||
task = text
|
||||
}
|
||||
return taskExecutionDirectives{
|
||||
task: task,
|
||||
stageReport: parsed.StageReport,
|
||||
}, true
|
||||
}
|
||||
|
||||
func parseAutonomyIntent(content string) (autonomyIntent, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
if strings.Contains(text, "停止自主模式") ||
|
||||
strings.Contains(text, "关闭自主模式") ||
|
||||
strings.Contains(text, "退出自主模式") ||
|
||||
strings.Contains(text, "别主动找我") ||
|
||||
strings.Contains(text, "不要主动找我") {
|
||||
return autonomyIntent{action: "stop"}, true
|
||||
}
|
||||
|
||||
if strings.Contains(text, "自主模式状态") ||
|
||||
strings.Contains(text, "查看自主模式") ||
|
||||
strings.Contains(text, "你现在是自主模式") {
|
||||
return autonomyIntent{action: "status"}, true
|
||||
}
|
||||
if strings.Contains(text, "方向执行完成") ||
|
||||
strings.Contains(text, "方向完成了") ||
|
||||
strings.Contains(text, "研究方向完成了") ||
|
||||
strings.Contains(text, "可以去执行别的") ||
|
||||
strings.Contains(text, "改做别的") ||
|
||||
strings.Contains(text, "先做其他") {
|
||||
return autonomyIntent{action: "clear_focus"}, true
|
||||
}
|
||||
|
||||
hasAutoAction := strings.Contains(text, "自动拆解") ||
|
||||
strings.Contains(text, "自动执行") ||
|
||||
strings.Contains(text, "主动找我") ||
|
||||
strings.Contains(text, "不用我一直问") ||
|
||||
strings.Contains(text, "你自己推进")
|
||||
hasPersistentHint := strings.Contains(text, "从现在开始") ||
|
||||
strings.Contains(text, "以后") ||
|
||||
strings.Contains(text, "长期") ||
|
||||
strings.Contains(text, "持续") ||
|
||||
strings.Contains(text, "一直") ||
|
||||
strings.Contains(text, "我不理你") ||
|
||||
strings.Contains(text, "我不说话") ||
|
||||
strings.Contains(text, "空闲时")
|
||||
|
||||
startHint := strings.Contains(text, "开启自主模式") ||
|
||||
strings.Contains(text, "开始自主模式") ||
|
||||
strings.Contains(text, "进入自主模式") ||
|
||||
strings.Contains(text, "启用自主模式") ||
|
||||
strings.Contains(text, "切到自主模式") ||
|
||||
(hasAutoAction && hasPersistentHint)
|
||||
|
||||
if !startHint {
|
||||
fields := strings.Fields(strings.ToLower(text))
|
||||
if len(fields) < 2 || fields[0] != "autonomy" {
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
|
||||
focus := extractAutonomyFocus(text)
|
||||
if d, ok := extractChineseAutoLearnInterval(text); ok {
|
||||
return autonomyIntent{action: "start", idleInterval: &d, focus: focus}, true
|
||||
}
|
||||
return autonomyIntent{action: "start", focus: focus}, true
|
||||
}
|
||||
|
||||
func extractAutonomyFocus(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
patterns := []string{
|
||||
"研究方向是",
|
||||
"研究方向:",
|
||||
"研究方向:",
|
||||
"方向是",
|
||||
"方向:",
|
||||
"方向:",
|
||||
"聚焦在",
|
||||
"重点研究",
|
||||
}
|
||||
for _, marker := range patterns {
|
||||
if idx := strings.Index(text, marker); idx >= 0 {
|
||||
focus := strings.TrimSpace(text[idx+len(marker):])
|
||||
focus = strings.Trim(focus, ",,。;; ")
|
||||
if cut := findFirstIndex(focus, "并且每", "然后", "每", "空闲", "主动", "汇报", "。"); cut > 0 {
|
||||
focus = strings.TrimSpace(focus[:cut])
|
||||
intent := autonomyIntent{action: fields[1]}
|
||||
if intent.action == "start" && len(fields) >= 3 {
|
||||
if d, err := parseAutonomyIdleInterval(fields[2]); err == nil {
|
||||
intent.idleInterval = &d
|
||||
if len(fields) >= 4 {
|
||||
intent.focus = strings.TrimSpace(strings.Join(strings.Fields(text)[3:], " "))
|
||||
}
|
||||
return strings.Trim(focus, ",,。;; ")
|
||||
} else {
|
||||
intent.focus = strings.TrimSpace(strings.Join(strings.Fields(text)[2:], " "))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
switch intent.action {
|
||||
case "start", "stop", "status", "clear_focus":
|
||||
return intent, true
|
||||
default:
|
||||
return autonomyIntent{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func parseAutoLearnIntent(content string) (autoLearnIntent, bool) {
|
||||
text := strings.TrimSpace(content)
|
||||
if text == "" || !strings.Contains(text, "自动学习") {
|
||||
fields := strings.Fields(strings.ToLower(text))
|
||||
if len(fields) < 2 || fields[0] != "autolearn" {
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
if strings.Contains(text, "停止自动学习") ||
|
||||
strings.Contains(text, "关闭自动学习") ||
|
||||
strings.Contains(text, "暂停自动学习") {
|
||||
return autoLearnIntent{action: "stop"}, true
|
||||
}
|
||||
|
||||
if strings.Contains(text, "自动学习状态") ||
|
||||
strings.Contains(text, "查看自动学习") ||
|
||||
strings.Contains(text, "自动学习还在") ||
|
||||
strings.Contains(text, "自动学习进度") {
|
||||
return autoLearnIntent{action: "status"}, true
|
||||
}
|
||||
|
||||
if strings.Contains(text, "开始自动学习") ||
|
||||
strings.Contains(text, "开启自动学习") ||
|
||||
strings.Contains(text, "启动自动学习") ||
|
||||
strings.Contains(text, "打开自动学习") {
|
||||
if d, ok := extractChineseAutoLearnInterval(text); ok {
|
||||
return autoLearnIntent{action: "start", interval: &d}, true
|
||||
intent := autoLearnIntent{action: fields[1]}
|
||||
if intent.action == "start" && len(fields) >= 3 {
|
||||
if d, err := parseAutoLearnInterval(fields[2]); err == nil {
|
||||
intent.interval = &d
|
||||
}
|
||||
return autoLearnIntent{action: "start"}, true
|
||||
}
|
||||
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
func extractChineseAutoLearnInterval(text string) (time.Duration, bool) {
|
||||
patterns := []struct {
|
||||
re *regexp.Regexp
|
||||
unit time.Duration
|
||||
}{
|
||||
{re: regexp.MustCompile(`每\s*(\d+)\s*秒`), unit: time.Second},
|
||||
{re: regexp.MustCompile(`每\s*(\d+)\s*分钟`), unit: time.Minute},
|
||||
{re: regexp.MustCompile(`每\s*(\d+)\s*小时`), unit: time.Hour},
|
||||
switch intent.action {
|
||||
case "start", "stop", "status":
|
||||
return intent, true
|
||||
default:
|
||||
return autoLearnIntent{}, false
|
||||
}
|
||||
|
||||
for _, p := range patterns {
|
||||
m := p.re.FindStringSubmatch(text)
|
||||
if len(m) != 2 {
|
||||
continue
|
||||
}
|
||||
n, err := strconv.Atoi(m[1])
|
||||
if err != nil || n <= 0 {
|
||||
continue
|
||||
}
|
||||
return time.Duration(n) * p.unit, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (al *AgentLoop) handleSlashCommand(msg bus.InboundMessage) (bool, string, error) {
|
||||
|
||||
@@ -15,16 +15,6 @@ func TestParseTaskExecutionDirectives_RunCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTaskExecutionDirectives_NaturalLanguage(t *testing.T) {
|
||||
d := parseTaskExecutionDirectives("你可以自动运行任务:整理日志,但是每到一个阶段给我报告一下任务完成情况")
|
||||
if d.task != "整理日志" {
|
||||
t.Fatalf("unexpected task: %q", d.task)
|
||||
}
|
||||
if !d.stageReport {
|
||||
t.Fatalf("expected stage report enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTaskExecutionDirectives_Default(t *testing.T) {
|
||||
d := parseTaskExecutionDirectives("帮我看看今天的日志异常")
|
||||
if d.task != "帮我看看今天的日志异常" {
|
||||
@@ -59,8 +49,8 @@ func TestParseAutoLearnInterval_Invalid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoLearnIntent_StartNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutoLearnIntent("请开始自动学习,每5分钟执行一轮")
|
||||
func TestParseAutoLearnIntent_FallbackCommand(t *testing.T) {
|
||||
intent, ok := parseAutoLearnIntent("autolearn start 5m")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
@@ -72,8 +62,8 @@ func TestParseAutoLearnIntent_StartNaturalLanguage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoLearnIntent_StopNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutoLearnIntent("先暂停自动学习")
|
||||
func TestParseAutoLearnIntent_StopFallbackCommand(t *testing.T) {
|
||||
intent, ok := parseAutoLearnIntent("autolearn stop")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
@@ -82,18 +72,14 @@ func TestParseAutoLearnIntent_StopNaturalLanguage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutoLearnIntent_StatusNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutoLearnIntent("帮我看下自动学习状态")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
if intent.action != "status" {
|
||||
t.Fatalf("unexpected action: %s", intent.action)
|
||||
func TestParseAutoLearnIntent_NoNaturalLanguageFallback(t *testing.T) {
|
||||
if _, ok := parseAutoLearnIntent("请开始自动学习"); ok {
|
||||
t.Fatalf("expected no fallback match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_StartNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("以后你自动拆解并自动执行任务,每15分钟主动找我汇报一次,研究方向是日志异常聚类")
|
||||
func TestParseAutonomyIntent_FallbackCommand(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("autonomy start 15m log clustering")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
@@ -103,13 +89,13 @@ func TestParseAutonomyIntent_StartNaturalLanguage(t *testing.T) {
|
||||
if intent.idleInterval == nil || *intent.idleInterval != 15*time.Minute {
|
||||
t.Fatalf("unexpected interval: %v", intent.idleInterval)
|
||||
}
|
||||
if intent.focus != "日志异常聚类" {
|
||||
if intent.focus != "log clustering" {
|
||||
t.Fatalf("unexpected focus: %q", intent.focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_StopNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("先不要主动找我,关闭自主模式")
|
||||
func TestParseAutonomyIntent_StopFallbackCommand(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("autonomy stop")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
@@ -118,8 +104,8 @@ func TestParseAutonomyIntent_StopNaturalLanguage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_StatusNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("帮我看下自主模式状态")
|
||||
func TestParseAutonomyIntent_StatusFallbackCommand(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("autonomy status")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
@@ -138,28 +124,14 @@ func TestParseAutonomyIdleInterval(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_NoFalsePositiveOnSingleTask(t *testing.T) {
|
||||
func TestParseAutonomyIntent_NoNaturalLanguageFallback(t *testing.T) {
|
||||
if intent, ok := parseAutonomyIntent("请自动执行这个任务"); ok {
|
||||
t.Fatalf("expected no intent, got: %+v", intent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractAutonomyFocus_EmptyWhenNotProvided(t *testing.T) {
|
||||
focus := extractAutonomyFocus("开启自主模式,每30分钟主动汇报")
|
||||
if focus != "" {
|
||||
t.Fatalf("expected empty focus, got: %q", focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractAutonomyFocus_KeepInnerBing(t *testing.T) {
|
||||
focus := extractAutonomyFocus("开启自主模式,研究方向是日志聚类并关联异常根因,并且每30分钟主动汇报")
|
||||
if focus != "日志聚类并关联异常根因" {
|
||||
t.Fatalf("unexpected focus: %q", focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAutonomyIntent_ClearFocusNaturalLanguage(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("自主附带的方向执行完成了,可以去执行别的")
|
||||
func TestParseAutonomyIntent_ClearFocusFallbackCommand(t *testing.T) {
|
||||
intent, ok := parseAutonomyIntent("autonomy clear_focus")
|
||||
if !ok {
|
||||
t.Fatalf("expected intent")
|
||||
}
|
||||
@@ -167,3 +139,16 @@ func TestParseAutonomyIntent_ClearFocusNaturalLanguage(t *testing.T) {
|
||||
t.Fatalf("unexpected action: %s", intent.action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractJSONObject_FromCodeFence(t *testing.T) {
|
||||
raw := extractJSONObject("```json\n{\"action\":\"start\",\"confidence\":0.95}\n```")
|
||||
if raw != "{\"action\":\"start\",\"confidence\":0.95}" {
|
||||
t.Fatalf("unexpected json: %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractJSONObject_Invalid(t *testing.T) {
|
||||
if raw := extractJSONObject("no json here"); raw != "" {
|
||||
t.Fatalf("expected empty json, got: %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user