clouds-coder 2026.3.28__tar.gz → 2026.3.29__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/Clouds_Coder.py +1809 -66
- {clouds_coder-2026.3.28/clouds_coder.egg-info → clouds_coder-2026.3.29}/PKG-INFO +1 -1
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29/clouds_coder.egg-info}/PKG-INFO +1 -1
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/pyproject.toml +1 -1
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/LICENSE +0 -0
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/README.md +0 -0
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/clouds_coder.egg-info/SOURCES.txt +0 -0
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/clouds_coder.egg-info/dependency_links.txt +0 -0
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/clouds_coder.egg-info/entry_points.txt +0 -0
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/clouds_coder.egg-info/requires.txt +0 -0
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/clouds_coder.egg-info/top_level.txt +0 -0
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/setup.cfg +0 -0
- {clouds_coder-2026.3.28 → clouds_coder-2026.3.29}/tests/test_smoke.py +0 -0
|
@@ -448,6 +448,7 @@ SKILL_RESOURCE_MANIFEST_MAX_ITEMS = 120
|
|
|
448
448
|
SKILL_BODY_COMPACT_THRESHOLD_CHARS = 12_000
|
|
449
449
|
SKILL_BODY_PREVIEW_CHARS = 4_000
|
|
450
450
|
SKILLS_VIRTUAL_PREFIX = "/skills"
|
|
451
|
+
SKILLS_EXTERNAL_MOUNT = "__external__"
|
|
451
452
|
PLAN_MODE_ENABLED_LEVELS = {3, 4, 5}
|
|
452
453
|
PLAN_MODE_FORCED_LEVELS = {4, 5}
|
|
453
454
|
PLAN_MODE_USER_CHOICES = ("auto", "on", "off")
|
|
@@ -478,7 +479,7 @@ REVIEWER_DEBUG_TOOL_ALLOWLIST = {
|
|
|
478
479
|
}
|
|
479
480
|
EXPLORER_STALL_THRESHOLD = 3 # consecutive same-target delegations before forced switch
|
|
480
481
|
DEVELOPER_EDIT_STALL_THRESHOLD = 3 # consecutive edit_file failures on same file before forced strategy change
|
|
481
|
-
PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS =
|
|
482
|
+
PLAN_MODE_MANAGER_SYNTHESIS_MAX_TOKENS = 6144
|
|
482
483
|
PLAN_MODE_MAX_OPTIONS = 3
|
|
483
484
|
PLAN_FILE_RELATIVE_PATH = ".clouds_coder/plan.md"
|
|
484
485
|
PLAN_BUBBLE_MAX_CHARS = 3800 # margin under ASSISTANT_MESSAGE_EVENT_MAX_CHARS (4000)
|
|
@@ -1529,6 +1530,9 @@ def infer_model_multimodal_capabilities(provider: str, model: str) -> dict[str,
|
|
|
1529
1530
|
"qwen-vl",
|
|
1530
1531
|
"gemini",
|
|
1531
1532
|
"claude-3",
|
|
1533
|
+
"claude-sonnet-4",
|
|
1534
|
+
"claude-opus",
|
|
1535
|
+
"glm-4v",
|
|
1532
1536
|
"omni",
|
|
1533
1537
|
)
|
|
1534
1538
|
):
|
|
@@ -1543,6 +1547,10 @@ def infer_model_multimodal_capabilities(provider: str, model: str) -> dict[str,
|
|
|
1543
1547
|
caps["output_audio"] = True
|
|
1544
1548
|
if any(x in m for x in ("video", "sora", "kling", "wan")):
|
|
1545
1549
|
caps["output_video"] = True
|
|
1550
|
+
if p == "anthropic":
|
|
1551
|
+
# All current Claude models support image input
|
|
1552
|
+
if any(x in m for x in ("claude-3", "claude-sonnet-4", "claude-opus", "claude-haiku")):
|
|
1553
|
+
caps["input_image"] = True
|
|
1546
1554
|
return caps
|
|
1547
1555
|
|
|
1548
1556
|
|
|
@@ -3104,6 +3112,136 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
|
|
|
3104
3112
|
media_endpoints=build_profile_media_endpoints("siliconflow"),
|
|
3105
3113
|
)
|
|
3106
3114
|
|
|
3115
|
+
# ── vLLM (local) ────────────────────────────────────────���─────
|
|
3116
|
+
vllm_url = str(config.get("vllm_url", "")).strip()
|
|
3117
|
+
vllm_model = str(config.get("vllm_model", "")).strip()
|
|
3118
|
+
vllm_key = str(config.get("vllm_key", "")).strip()
|
|
3119
|
+
if vllm_url or vllm_model:
|
|
3120
|
+
_vllm_default = "http://localhost:8000/v1"
|
|
3121
|
+
add_profile(
|
|
3122
|
+
profiles,
|
|
3123
|
+
profile_id="vllm",
|
|
3124
|
+
provider="openai_compat",
|
|
3125
|
+
label="vLLM",
|
|
3126
|
+
model=vllm_model or "auto",
|
|
3127
|
+
base_url=extract_base_url(vllm_url or _vllm_default),
|
|
3128
|
+
endpoint=complete_chat_endpoint(vllm_url or _vllm_default),
|
|
3129
|
+
api_key=vllm_key,
|
|
3130
|
+
thinking_stream=bool(config.get("vllm_thinking_stream", thinking_stream_default)),
|
|
3131
|
+
temperature=temp,
|
|
3132
|
+
request_timeout=timeout,
|
|
3133
|
+
capabilities=build_profile_capabilities("vllm", "openai_compat", vllm_model or "auto"),
|
|
3134
|
+
media_endpoints=build_profile_media_endpoints("vllm"),
|
|
3135
|
+
)
|
|
3136
|
+
|
|
3137
|
+
# ── LM Studio (local) ─────────────────────────────────────────
|
|
3138
|
+
lms_url = str(config.get("lmstudio_url", "")).strip()
|
|
3139
|
+
lms_model = str(config.get("lmstudio_model", "")).strip()
|
|
3140
|
+
if lms_url or lms_model:
|
|
3141
|
+
_lms_default = "http://localhost:1234/v1"
|
|
3142
|
+
add_profile(
|
|
3143
|
+
profiles,
|
|
3144
|
+
profile_id="lmstudio",
|
|
3145
|
+
provider="openai_compat",
|
|
3146
|
+
label="LM Studio",
|
|
3147
|
+
model=lms_model or "auto",
|
|
3148
|
+
base_url=extract_base_url(lms_url or _lms_default),
|
|
3149
|
+
endpoint=complete_chat_endpoint(lms_url or _lms_default),
|
|
3150
|
+
thinking_stream=bool(config.get("lmstudio_thinking_stream", thinking_stream_default)),
|
|
3151
|
+
temperature=temp,
|
|
3152
|
+
request_timeout=timeout,
|
|
3153
|
+
capabilities=build_profile_capabilities("lmstudio", "openai_compat", lms_model or "auto"),
|
|
3154
|
+
media_endpoints=build_profile_media_endpoints("lmstudio"),
|
|
3155
|
+
)
|
|
3156
|
+
|
|
3157
|
+
# ── Anthropic ──────────────────────────────────────────────────
|
|
3158
|
+
anth_url = str(config.get("anthropic_url", "")).strip()
|
|
3159
|
+
anth_model = str(config.get("anthropic_model", "")).strip()
|
|
3160
|
+
anth_key = str(config.get("anthropic_key", "")).strip()
|
|
3161
|
+
if anth_url or anth_model or anth_key:
|
|
3162
|
+
_anth_base = anth_url or "https://api.anthropic.com"
|
|
3163
|
+
add_profile(
|
|
3164
|
+
profiles,
|
|
3165
|
+
profile_id="anthropic",
|
|
3166
|
+
provider="anthropic",
|
|
3167
|
+
label="Anthropic",
|
|
3168
|
+
model=anth_model or "claude-sonnet-4-20250514",
|
|
3169
|
+
base_url=extract_base_url(_anth_base),
|
|
3170
|
+
endpoint=_anth_base.rstrip("/") + "/v1/messages",
|
|
3171
|
+
api_key=anth_key,
|
|
3172
|
+
thinking_stream=bool(config.get("anthropic_thinking_stream", thinking_stream_default)),
|
|
3173
|
+
temperature=temp,
|
|
3174
|
+
request_timeout=timeout,
|
|
3175
|
+
capabilities=build_profile_capabilities("anthropic", "anthropic", anth_model or "claude-sonnet-4-20250514"),
|
|
3176
|
+
media_endpoints=build_profile_media_endpoints("anthropic"),
|
|
3177
|
+
)
|
|
3178
|
+
|
|
3179
|
+
# ── GLM (智谱) ─────────────────────────────────────────────────
|
|
3180
|
+
glm_url = str(config.get("glm_url", "")).strip()
|
|
3181
|
+
glm_model = str(config.get("glm_model", "")).strip()
|
|
3182
|
+
glm_key = str(config.get("glm_key", "")).strip()
|
|
3183
|
+
if glm_url or glm_model or glm_key:
|
|
3184
|
+
_glm_default = "https://open.bigmodel.cn/api/paas/v4"
|
|
3185
|
+
add_profile(
|
|
3186
|
+
profiles,
|
|
3187
|
+
profile_id="glm",
|
|
3188
|
+
provider="openai_compat",
|
|
3189
|
+
label="GLM",
|
|
3190
|
+
model=glm_model or "glm-4-flash",
|
|
3191
|
+
base_url=extract_base_url(glm_url or _glm_default),
|
|
3192
|
+
endpoint=complete_chat_endpoint(glm_url or _glm_default),
|
|
3193
|
+
api_key=glm_key,
|
|
3194
|
+
thinking_stream=bool(config.get("glm_thinking_stream", thinking_stream_default)),
|
|
3195
|
+
temperature=temp,
|
|
3196
|
+
request_timeout=timeout,
|
|
3197
|
+
capabilities=build_profile_capabilities("glm", "openai_compat", glm_model or "glm-4-flash"),
|
|
3198
|
+
media_endpoints=build_profile_media_endpoints("glm"),
|
|
3199
|
+
)
|
|
3200
|
+
|
|
3201
|
+
# ── KIMI (Moonshot / 月之暗面) ─────────────────────────────────
|
|
3202
|
+
kimi_url = str(config.get("kimi_url", "")).strip()
|
|
3203
|
+
kimi_model = str(config.get("kimi_model", "")).strip()
|
|
3204
|
+
kimi_key = str(config.get("kimi_key", "")).strip()
|
|
3205
|
+
if kimi_url or kimi_model or kimi_key:
|
|
3206
|
+
_kimi_default = "https://api.moonshot.cn/v1"
|
|
3207
|
+
add_profile(
|
|
3208
|
+
profiles,
|
|
3209
|
+
profile_id="kimi",
|
|
3210
|
+
provider="openai_compat",
|
|
3211
|
+
label="KIMI (Moonshot)",
|
|
3212
|
+
model=kimi_model or "moonshot-v1-8k",
|
|
3213
|
+
base_url=extract_base_url(kimi_url or _kimi_default),
|
|
3214
|
+
endpoint=complete_chat_endpoint(kimi_url or _kimi_default),
|
|
3215
|
+
api_key=kimi_key,
|
|
3216
|
+
thinking_stream=bool(config.get("kimi_thinking_stream", thinking_stream_default)),
|
|
3217
|
+
temperature=temp,
|
|
3218
|
+
request_timeout=timeout,
|
|
3219
|
+
capabilities=build_profile_capabilities("kimi", "openai_compat", kimi_model or "moonshot-v1-8k"),
|
|
3220
|
+
media_endpoints=build_profile_media_endpoints("kimi"),
|
|
3221
|
+
)
|
|
3222
|
+
|
|
3223
|
+
# ── OpenRouter ─────────────────────────────────────────────────
|
|
3224
|
+
or_url = str(config.get("openrouter_url", "")).strip()
|
|
3225
|
+
or_model = str(config.get("openrouter_model", "")).strip()
|
|
3226
|
+
or_key = str(config.get("openrouter_key", "")).strip()
|
|
3227
|
+
if or_url or or_model or or_key:
|
|
3228
|
+
_or_default = "https://openrouter.ai/api/v1"
|
|
3229
|
+
add_profile(
|
|
3230
|
+
profiles,
|
|
3231
|
+
profile_id="openrouter",
|
|
3232
|
+
provider="openai_compat",
|
|
3233
|
+
label="OpenRouter",
|
|
3234
|
+
model=or_model or "meta-llama/llama-3.1-8b-instruct",
|
|
3235
|
+
base_url=extract_base_url(or_url or _or_default),
|
|
3236
|
+
endpoint=complete_chat_endpoint(or_url or _or_default),
|
|
3237
|
+
api_key=or_key,
|
|
3238
|
+
thinking_stream=bool(config.get("openrouter_thinking_stream", thinking_stream_default)),
|
|
3239
|
+
temperature=temp,
|
|
3240
|
+
request_timeout=timeout,
|
|
3241
|
+
capabilities=build_profile_capabilities("openrouter", "openai_compat", or_model or "meta-llama/llama-3.1-8b-instruct"),
|
|
3242
|
+
media_endpoints=build_profile_media_endpoints("openrouter"),
|
|
3243
|
+
)
|
|
3244
|
+
|
|
3107
3245
|
custom_url = str(config.get("custom_url", "")).strip()
|
|
3108
3246
|
custom_key = str(config.get("custom_key", "")).strip()
|
|
3109
3247
|
custom_headers = parse_json_object(str(config.get("custom_headers", "{}") or "{}"), {})
|
|
@@ -3147,9 +3285,24 @@ def parse_llm_config_profiles(config: dict, default_ollama_url: str, default_oll
|
|
|
3147
3285
|
"ollama": "ollama",
|
|
3148
3286
|
"openai": "openai",
|
|
3149
3287
|
"siliconflow": "siliconflow",
|
|
3288
|
+
"vllm": "vllm",
|
|
3289
|
+
"lmstudio": "lmstudio",
|
|
3290
|
+
"anthropic": "anthropic",
|
|
3291
|
+
"glm": "glm",
|
|
3292
|
+
"kimi": "kimi",
|
|
3293
|
+
"openrouter": "openrouter",
|
|
3150
3294
|
"custom": "custom",
|
|
3151
3295
|
}
|
|
3152
|
-
|
|
3296
|
+
profile_ids = {p["id"] for p in profiles}
|
|
3297
|
+
default_profile_id = active_map.get(provider, "")
|
|
3298
|
+
if not default_profile_id or default_profile_id not in profile_ids:
|
|
3299
|
+
# Fallback: first non-ollama profile that was explicitly configured
|
|
3300
|
+
for p in profiles:
|
|
3301
|
+
if p["id"] != "ollama" and p.get("source") != "default":
|
|
3302
|
+
default_profile_id = p["id"]
|
|
3303
|
+
break
|
|
3304
|
+
if not default_profile_id or default_profile_id not in profile_ids:
|
|
3305
|
+
default_profile_id = profiles[0]["id"]
|
|
3153
3306
|
return {"profiles": profiles, "default_profile_id": default_profile_id}
|
|
3154
3307
|
|
|
3155
3308
|
def looks_like_llm_config(config: dict) -> bool:
|
|
@@ -3166,6 +3319,22 @@ def looks_like_llm_config(config: dict) -> bool:
|
|
|
3166
3319
|
"siliconflow_url",
|
|
3167
3320
|
"siliconflow_model",
|
|
3168
3321
|
"siliconflow_key",
|
|
3322
|
+
"vllm_url",
|
|
3323
|
+
"vllm_model",
|
|
3324
|
+
"lmstudio_url",
|
|
3325
|
+
"lmstudio_model",
|
|
3326
|
+
"anthropic_url",
|
|
3327
|
+
"anthropic_model",
|
|
3328
|
+
"anthropic_key",
|
|
3329
|
+
"glm_url",
|
|
3330
|
+
"glm_model",
|
|
3331
|
+
"glm_key",
|
|
3332
|
+
"kimi_url",
|
|
3333
|
+
"kimi_model",
|
|
3334
|
+
"kimi_key",
|
|
3335
|
+
"openrouter_url",
|
|
3336
|
+
"openrouter_model",
|
|
3337
|
+
"openrouter_key",
|
|
3169
3338
|
"custom_url",
|
|
3170
3339
|
"custom_model",
|
|
3171
3340
|
"custom_key",
|
|
@@ -3178,6 +3347,12 @@ def looks_like_llm_config(config: dict) -> bool:
|
|
|
3178
3347
|
"ollama_capabilities",
|
|
3179
3348
|
"openai_capabilities",
|
|
3180
3349
|
"siliconflow_capabilities",
|
|
3350
|
+
"anthropic_capabilities",
|
|
3351
|
+
"glm_capabilities",
|
|
3352
|
+
"kimi_capabilities",
|
|
3353
|
+
"openrouter_capabilities",
|
|
3354
|
+
"vllm_capabilities",
|
|
3355
|
+
"lmstudio_capabilities",
|
|
3181
3356
|
"custom_capabilities",
|
|
3182
3357
|
"ollama_media_endpoints",
|
|
3183
3358
|
"openai_media_endpoints",
|
|
@@ -6416,6 +6591,1004 @@ if __name__ == "__main__":
|
|
|
6416
6591
|
),
|
|
6417
6592
|
)
|
|
6418
6593
|
|
|
6594
|
+
# ---------------------------------------------------------------------------
|
|
6595
|
+
# Generated Skills: RAG Retrieval Mastery
|
|
6596
|
+
# ---------------------------------------------------------------------------
|
|
6597
|
+
|
|
6598
|
+
def ensure_generated_rag_mastery_skills(skills_root: Path):
|
|
6599
|
+
generated_root = skills_root / "generated"
|
|
6600
|
+
|
|
6601
|
+
# ── Skill A: rag-retrieval-mastery ────────────────────────────────
|
|
6602
|
+
|
|
6603
|
+
rag_mastery_skill = """---
|
|
6604
|
+
name: rag-retrieval-mastery
|
|
6605
|
+
aliases:
|
|
6606
|
+
- rag-mastery
|
|
6607
|
+
- rag-guide
|
|
6608
|
+
- retrieval-mastery
|
|
6609
|
+
triggers:
|
|
6610
|
+
- RAG query
|
|
6611
|
+
- knowledge library
|
|
6612
|
+
- retrieval strategy
|
|
6613
|
+
- query formulation
|
|
6614
|
+
- cross-library search
|
|
6615
|
+
- empty retrieval results
|
|
6616
|
+
- RAG检索
|
|
6617
|
+
- 知识库查询
|
|
6618
|
+
- 检索策略
|
|
6619
|
+
- 查询优化
|
|
6620
|
+
clouds_coder:
|
|
6621
|
+
preferred_tools:
|
|
6622
|
+
- query_knowledge_library
|
|
6623
|
+
- query_code_library
|
|
6624
|
+
- read_file
|
|
6625
|
+
description: >
|
|
6626
|
+
Comprehensive guide for effective RAG retrieval across knowledge and code libraries.
|
|
6627
|
+
Covers query formulation, route selection, iterative refinement, cross-library strategy,
|
|
6628
|
+
result interpretation, and citation best practices.
|
|
6629
|
+
TRIGGER when: user asks about RAG usage, retrieval quality is poor, empty results from library queries,
|
|
6630
|
+
or task requires grounded knowledge retrieval before answering.
|
|
6631
|
+
DO NOT TRIGGER for: general research workflows (use research-orchestrator-pro), scientific reasoning.
|
|
6632
|
+
---
|
|
6633
|
+
|
|
6634
|
+
# RAG Retrieval Mastery
|
|
6635
|
+
|
|
6636
|
+
Master guide for effective use of `query_knowledge_library` and `query_code_library`.
|
|
6637
|
+
|
|
6638
|
+
## When to Use
|
|
6639
|
+
- Before answering questions that may have grounded references in the library
|
|
6640
|
+
- When retrieval results are empty or irrelevant
|
|
6641
|
+
- When combining knowledge from code and document libraries
|
|
6642
|
+
- When you need to formulate complex multi-step queries
|
|
6643
|
+
|
|
6644
|
+
## 1. Query Formulation Strategies
|
|
6645
|
+
|
|
6646
|
+
### Entity-Focused Query
|
|
6647
|
+
Extract the **core entity/concept** from the user's question:
|
|
6648
|
+
- Bad: "What does the system do when a user logs in?"
|
|
6649
|
+
- Good: `query_knowledge_library(query="user login authentication flow", top_k=8)`
|
|
6650
|
+
|
|
6651
|
+
### Concept Decomposition
|
|
6652
|
+
Break complex questions into 2-3 sub-queries:
|
|
6653
|
+
1. `query_knowledge_library(query="transformer attention mechanism", top_k=5)`
|
|
6654
|
+
2. `query_knowledge_library(query="self-attention vs cross-attention", top_k=5)`
|
|
6655
|
+
|
|
6656
|
+
### Specificity Ladder
|
|
6657
|
+
Start specific, broaden if empty:
|
|
6658
|
+
1. `query="BERT fine-tuning learning rate schedule"` (narrow)
|
|
6659
|
+
2. `query="BERT fine-tuning hyperparameters"` (medium)
|
|
6660
|
+
3. `query="transformer model fine-tuning"` (broad)
|
|
6661
|
+
|
|
6662
|
+
### Synonym Expansion
|
|
6663
|
+
If first query returns few results, try synonyms:
|
|
6664
|
+
- "machine learning" → "ML", "deep learning", "neural network"
|
|
6665
|
+
- "API endpoint" → "REST route", "HTTP handler", "web service"
|
|
6666
|
+
|
|
6667
|
+
### Multilingual Bridging (CN/EN)
|
|
6668
|
+
Query in **both languages** if the library may contain mixed content:
|
|
6669
|
+
1. `query="注意力机制 attention mechanism"` (combined)
|
|
6670
|
+
2. Or run two queries: one Chinese, one English, merge results
|
|
6671
|
+
|
|
6672
|
+
### Negation-Aware Query
|
|
6673
|
+
To find counter-evidence or alternatives:
|
|
6674
|
+
- `query="limitations of batch normalization alternatives"` (includes both positive and negative)
|
|
6675
|
+
|
|
6676
|
+
## 2. Route Selection Decision Table
|
|
6677
|
+
|
|
6678
|
+
| Route | When to Use | Example |
|
|
6679
|
+
|-------|-------------|---------|
|
|
6680
|
+
| `fast` | Exact terms, IDs, specific names, short lookup | `query="ResNet-50 accuracy ImageNet", route="fast"` |
|
|
6681
|
+
| `global` | Broad themes, overview, synthesis across topics | `query="deep learning trends 2024", route="global"` |
|
|
6682
|
+
| `hybrid` | Best default — combines keyword + semantic | `query="transformer optimization techniques", route="hybrid"` |
|
|
6683
|
+
| `auto` | Unsure — let system decide | `query="attention", route="auto"` |
|
|
6684
|
+
|
|
6685
|
+
**Default recommendation: always start with `route="hybrid"`** unless you have a specific reason for another route.
|
|
6686
|
+
|
|
6687
|
+
## 3. Iterative Refinement Protocol
|
|
6688
|
+
|
|
6689
|
+
When results are empty or irrelevant, follow this escalation:
|
|
6690
|
+
|
|
6691
|
+
```
|
|
6692
|
+
Step 1: Try synonyms/alternative terms (same route)
|
|
6693
|
+
→ Still empty?
|
|
6694
|
+
Step 2: Switch route (fast→hybrid, or hybrid→global)
|
|
6695
|
+
→ Still empty?
|
|
6696
|
+
Step 3: Broaden query (remove modifiers, use parent concept)
|
|
6697
|
+
→ Still empty?
|
|
6698
|
+
Step 4: Check library status (is it initialized? has documents?)
|
|
6699
|
+
→ query_knowledge_library(query="*", top_k=1) to test readiness
|
|
6700
|
+
```
|
|
6701
|
+
|
|
6702
|
+
**Never give up after one empty query.** Always try at least 2-3 variations before concluding the library has no relevant content.
|
|
6703
|
+
|
|
6704
|
+
## 4. Cross-Library Strategy
|
|
6705
|
+
|
|
6706
|
+
### When to use Knowledge Library
|
|
6707
|
+
- Research papers, reports, uploaded documents
|
|
6708
|
+
- Domain knowledge, definitions, benchmarks
|
|
6709
|
+
- Historical data, reference material
|
|
6710
|
+
|
|
6711
|
+
### When to use Code Library
|
|
6712
|
+
- Function implementations, API signatures
|
|
6713
|
+
- Code patterns, architectural decisions
|
|
6714
|
+
- Error handling patterns, test examples
|
|
6715
|
+
|
|
6716
|
+
### Combined workflow
|
|
6717
|
+
1. Knowledge first: `query_knowledge_library(query="OAuth 2.0 PKCE flow", route="hybrid")`
|
|
6718
|
+
2. Code second: `query_code_library(query="oauth pkce implementation", route="fast")`
|
|
6719
|
+
3. Merge: Use knowledge context to understand the code, use code to ground the theory
|
|
6720
|
+
|
|
6721
|
+
## 5. Result Interpretation
|
|
6722
|
+
|
|
6723
|
+
### Score Thresholds
|
|
6724
|
+
- score > 0.7: Strong match — high confidence
|
|
6725
|
+
- score 0.4-0.7: Moderate match — review carefully
|
|
6726
|
+
- score < 0.4: Weak match — may be tangential
|
|
6727
|
+
|
|
6728
|
+
### Community Cards
|
|
6729
|
+
When results include `community_cards`:
|
|
6730
|
+
- Multiple communities = topic spans multiple areas → consider refining query
|
|
6731
|
+
- Single community = focused topic → results are likely relevant
|
|
6732
|
+
|
|
6733
|
+
### Empty Results Debugging
|
|
6734
|
+
1. Check `ready` field in library status
|
|
6735
|
+
2. Try `query="*", top_k=1` to verify library has content
|
|
6736
|
+
3. Check if documents were imported (documents > 0)
|
|
6737
|
+
4. Try simpler single-word queries to find what terms the library contains
|
|
6738
|
+
|
|
6739
|
+
## 6. Citation Best Practices
|
|
6740
|
+
- Always include the `citation` field from results in your response
|
|
6741
|
+
- Format: [Source: citation_string] after the referenced claim
|
|
6742
|
+
- For multiple sources supporting one claim, list all citations
|
|
6743
|
+
- If `title` differs from `citation`, prefer `citation` for attribution
|
|
6744
|
+
|
|
6745
|
+
## 7. Common Pitfalls
|
|
6746
|
+
- **Overly broad queries**: "tell me everything" → returns noise. Be specific.
|
|
6747
|
+
- **Wrong route**: Using `fast` for conceptual questions → misses semantic matches.
|
|
6748
|
+
- **Single attempt**: One query with no refinement → likely misses relevant content.
|
|
6749
|
+
- **Ignoring low-score results**: Sometimes the 8th result has the answer. Scan all returned results.
|
|
6750
|
+
- **Not using top_k**: Default may be too low. Use `top_k=10` for exploration, `top_k=5` for focused.
|
|
6751
|
+
"""
|
|
6752
|
+
|
|
6753
|
+
# ── Skill B: code-library-navigator ───────────────────────────────
|
|
6754
|
+
|
|
6755
|
+
code_nav_skill = """---
|
|
6756
|
+
name: code-library-navigator
|
|
6757
|
+
aliases:
|
|
6758
|
+
- code-navigator
|
|
6759
|
+
- code-rag
|
|
6760
|
+
- code-search
|
|
6761
|
+
triggers:
|
|
6762
|
+
- code library
|
|
6763
|
+
- code search
|
|
6764
|
+
- function lookup
|
|
6765
|
+
- symbol search
|
|
6766
|
+
- API reference
|
|
6767
|
+
- code review with library
|
|
6768
|
+
- 代码库查询
|
|
6769
|
+
- 代码搜索
|
|
6770
|
+
- 函数查找
|
|
6771
|
+
- 符号搜索
|
|
6772
|
+
clouds_coder:
|
|
6773
|
+
preferred_tools:
|
|
6774
|
+
- query_code_library
|
|
6775
|
+
- read_file
|
|
6776
|
+
- bash
|
|
6777
|
+
description: >
|
|
6778
|
+
Specialized guide for code library retrieval. Code-specific query patterns,
|
|
6779
|
+
language filtering, symbol-aware search, and integration with read_file for full context.
|
|
6780
|
+
TRIGGER when: looking up code implementations, function signatures, API patterns, code review.
|
|
6781
|
+
DO NOT TRIGGER for: knowledge/document retrieval (use rag-retrieval-mastery),
|
|
6782
|
+
general research (use research-orchestrator-pro).
|
|
6783
|
+
---
|
|
6784
|
+
|
|
6785
|
+
# Code Library Navigator
|
|
6786
|
+
|
|
6787
|
+
Specialized guide for effective code library retrieval via `query_code_library`.
|
|
6788
|
+
|
|
6789
|
+
## When to Use
|
|
6790
|
+
- Looking up function implementations or API signatures
|
|
6791
|
+
- Finding code patterns or architectural references
|
|
6792
|
+
- Code review with library-backed evidence
|
|
6793
|
+
- Understanding unfamiliar codebases via indexed symbols
|
|
6794
|
+
|
|
6795
|
+
## 1. Code-Specific Query Patterns
|
|
6796
|
+
|
|
6797
|
+
### Function/Method Lookup
|
|
6798
|
+
```
|
|
6799
|
+
query_code_library(query="def authenticate_user", route="fast")
|
|
6800
|
+
query_code_library(query="handleSubmit onClick", route="fast", language="javascript")
|
|
6801
|
+
```
|
|
6802
|
+
|
|
6803
|
+
### Class/Module Discovery
|
|
6804
|
+
```
|
|
6805
|
+
query_code_library(query="class UserRepository database", route="hybrid")
|
|
6806
|
+
query_code_library(query="module authentication middleware", route="hybrid")
|
|
6807
|
+
```
|
|
6808
|
+
|
|
6809
|
+
### API Pattern Search
|
|
6810
|
+
```
|
|
6811
|
+
query_code_library(query="REST endpoint /api/users POST", route="fast")
|
|
6812
|
+
query_code_library(query="GraphQL resolver mutation", route="hybrid")
|
|
6813
|
+
```
|
|
6814
|
+
|
|
6815
|
+
### Error Handling Patterns
|
|
6816
|
+
```
|
|
6817
|
+
query_code_library(query="try catch database connection retry", route="hybrid")
|
|
6818
|
+
query_code_library(query="error boundary fallback component", route="fast", language="typescript")
|
|
6819
|
+
```
|
|
6820
|
+
|
|
6821
|
+
### Algorithm/Logic Search
|
|
6822
|
+
```
|
|
6823
|
+
query_code_library(query="binary search sorted array", route="hybrid")
|
|
6824
|
+
query_code_library(query="LRU cache eviction policy", route="hybrid")
|
|
6825
|
+
```
|
|
6826
|
+
|
|
6827
|
+
## 2. Language Filtering
|
|
6828
|
+
|
|
6829
|
+
Use the `language` parameter to narrow results:
|
|
6830
|
+
```
|
|
6831
|
+
query_code_library(query="sort implementation", language="python")
|
|
6832
|
+
query_code_library(query="async await pattern", language="javascript")
|
|
6833
|
+
query_code_library(query="error handling", language="go")
|
|
6834
|
+
```
|
|
6835
|
+
|
|
6836
|
+
For multi-language projects, run **two queries** — one filtered, one unfiltered — and compare.
|
|
6837
|
+
|
|
6838
|
+
## 3. From Snippet to Full Context
|
|
6839
|
+
|
|
6840
|
+
Code library returns **snippets** (320 chars max). To get full context:
|
|
6841
|
+
|
|
6842
|
+
1. **Query**: `result = query_code_library(query="parse config file", top_k=5)`
|
|
6843
|
+
2. **Extract path**: Read `citation` field → contains file path
|
|
6844
|
+
3. **Read full file**: `read_file` on the extracted path
|
|
6845
|
+
4. **Analyze**: Now you have the full function/class context
|
|
6846
|
+
|
|
6847
|
+
This is the core workflow: **search → locate → read → understand**.
|
|
6848
|
+
|
|
6849
|
+
## 4. Symbol-Aware Search
|
|
6850
|
+
|
|
6851
|
+
Results may include a `symbol` field (function/class name):
|
|
6852
|
+
- Use symbol for precise follow-up: `query_code_library(query="symbol:UserService.create")`
|
|
6853
|
+
- Symbol results have higher precision than text-only matches
|
|
6854
|
+
|
|
6855
|
+
## 5. Code Review Workflow
|
|
6856
|
+
|
|
6857
|
+
1. **Find reference implementations**:
|
|
6858
|
+
`query_code_library(query="similar function pattern", top_k=8)`
|
|
6859
|
+
2. **Compare patterns**: Check if the code under review follows library conventions
|
|
6860
|
+
3. **Report deviations**: Note differences in error handling, naming, structure
|
|
6861
|
+
4. **Verify edge cases**: Query for test files: `query_code_library(query="test_authenticate", route="fast")`
|
|
6862
|
+
|
|
6863
|
+
## 6. Integration with Knowledge Library
|
|
6864
|
+
|
|
6865
|
+
When a code question needs **domain context**:
|
|
6866
|
+
1. First: `query_knowledge_library(query="OAuth 2.0 PKCE specification")` — understand the concept
|
|
6867
|
+
2. Then: `query_code_library(query="pkce code_verifier code_challenge")` — find the implementation
|
|
6868
|
+
3. Combine: Verify implementation matches specification
|
|
6869
|
+
|
|
6870
|
+
## Quick Reference
|
|
6871
|
+
|
|
6872
|
+
```
|
|
6873
|
+
query_code_library(
|
|
6874
|
+
query="search text", # Required: the search query
|
|
6875
|
+
top_k=8, # Optional: results count (1-12, default 8)
|
|
6876
|
+
route="hybrid", # Optional: fast|global|hybrid|auto
|
|
6877
|
+
language="python", # Optional: filter by programming language
|
|
6878
|
+
)
|
|
6879
|
+
```
|
|
6880
|
+
"""
|
|
6881
|
+
|
|
6882
|
+
_write_text_if_changed(generated_root / "rag-retrieval-mastery" / "SKILL.md", rag_mastery_skill)
|
|
6883
|
+
_write_text_if_changed(generated_root / "code-library-navigator" / "SKILL.md", code_nav_skill)
|
|
6884
|
+
_write_text_if_changed(
|
|
6885
|
+
generated_root / "rag-mastery-capabilities.json",
|
|
6886
|
+
json_dumps(
|
|
6887
|
+
{
|
|
6888
|
+
"generated_at": int(now_ts()),
|
|
6889
|
+
"skills": ["rag-retrieval-mastery", "code-library-navigator"],
|
|
6890
|
+
"focus": ["rag_query_mastery", "code_library_navigation"],
|
|
6891
|
+
},
|
|
6892
|
+
indent=2,
|
|
6893
|
+
),
|
|
6894
|
+
)
|
|
6895
|
+
|
|
6896
|
+
# ---------------------------------------------------------------------------
|
|
6897
|
+
# Generated Skills: Multimodal Reading Comprehension
|
|
6898
|
+
# ---------------------------------------------------------------------------
|
|
6899
|
+
|
|
6900
|
+
def ensure_generated_multimodal_comprehension_skills(skills_root: Path):
|
|
6901
|
+
generated_root = skills_root / "generated"
|
|
6902
|
+
|
|
6903
|
+
# ── Skill C: pdf-reading-comprehension ────────────────────────────
|
|
6904
|
+
|
|
6905
|
+
pdf_skill = """---
|
|
6906
|
+
name: pdf-reading-comprehension
|
|
6907
|
+
aliases:
|
|
6908
|
+
- pdf-reader
|
|
6909
|
+
- pdf-comprehension
|
|
6910
|
+
- read-pdf
|
|
6911
|
+
triggers:
|
|
6912
|
+
- read PDF
|
|
6913
|
+
- PDF analysis
|
|
6914
|
+
- PDF summary
|
|
6915
|
+
- PDF comparison
|
|
6916
|
+
- extract from PDF
|
|
6917
|
+
- PDF table
|
|
6918
|
+
- PDF figure
|
|
6919
|
+
- 阅读PDF
|
|
6920
|
+
- PDF分析
|
|
6921
|
+
- PDF摘要
|
|
6922
|
+
- PDF对比
|
|
6923
|
+
- PDF提取
|
|
6924
|
+
clouds_coder:
|
|
6925
|
+
preferred_tools:
|
|
6926
|
+
- read_file
|
|
6927
|
+
- bash
|
|
6928
|
+
- query_knowledge_library
|
|
6929
|
+
description: >
|
|
6930
|
+
General-purpose PDF reading comprehension skill. Text extraction via .parsed.md,
|
|
6931
|
+
figure analysis via read_file on extracted images, structure extraction, multi-PDF comparison,
|
|
6932
|
+
and summary generation at three depth levels.
|
|
6933
|
+
TRIGGER when: user uploads PDF and asks for reading/analysis/summary/comparison/extraction.
|
|
6934
|
+
DO NOT TRIGGER for: research literature review (use pdf-vision-literature-integrator),
|
|
6935
|
+
PDF creation/generation (use kimi-pdf or pdf skill).
|
|
6936
|
+
---
|
|
6937
|
+
|
|
6938
|
+
# PDF Reading Comprehension
|
|
6939
|
+
|
|
6940
|
+
General-purpose skill for reading and understanding PDF documents.
|
|
6941
|
+
|
|
6942
|
+
## When to Use
|
|
6943
|
+
- User uploads a PDF and asks to read, summarize, analyze, or compare it
|
|
6944
|
+
- Need to extract tables, figures, or specific sections from PDFs
|
|
6945
|
+
- Multi-PDF comparison or cross-reference tasks
|
|
6946
|
+
|
|
6947
|
+
## When NOT to Use
|
|
6948
|
+
- Academic literature review with evidence synthesis → use `pdf-vision-literature-integrator`
|
|
6949
|
+
- Creating/generating PDF files → use `kimi-pdf` or `pdf` output skill
|
|
6950
|
+
|
|
6951
|
+
## Step 1: Text Extraction
|
|
6952
|
+
|
|
6953
|
+
**Always try `.parsed.md` first** — the backend auto-generates it on upload:
|
|
6954
|
+
```
|
|
6955
|
+
read_file uploaded/<filename>.parsed.md
|
|
6956
|
+
```
|
|
6957
|
+
|
|
6958
|
+
If `.parsed.md` is unavailable or incomplete:
|
|
6959
|
+
```bash
|
|
6960
|
+
# pdftotext (preserves layout)
|
|
6961
|
+
bash: pdftotext -layout "uploaded/<filename>.pdf" -
|
|
6962
|
+
|
|
6963
|
+
# pdfminer (Python, more robust)
|
|
6964
|
+
bash: python3 -c "from pdfminer.high_level import extract_text; print(extract_text('uploaded/<filename>.pdf'))"
|
|
6965
|
+
```
|
|
6966
|
+
|
|
6967
|
+
**Scanned PDF detection**: If extracted text is empty, garbled, or very short relative to page count,
|
|
6968
|
+
the PDF is likely scanned → use OCR fallback (Step 5).
|
|
6969
|
+
|
|
6970
|
+
## Step 2: Structure Recognition
|
|
6971
|
+
|
|
6972
|
+
After reading text, identify the document structure:
|
|
6973
|
+
1. **Headings/Sections**: Look for numbered sections, bold text, ALL CAPS patterns
|
|
6974
|
+
2. **Table of Contents**: Often in first 2 pages — use as navigation map
|
|
6975
|
+
3. **Tables**: Look for tab-separated or pipe-separated data in parsed text
|
|
6976
|
+
4. **Figure references**: "Figure 1", "Fig. 2", "Table 3" — note their locations
|
|
6977
|
+
5. **Page numbers**: Track content locations for citations
|
|
6978
|
+
|
|
6979
|
+
## Step 3: Visual Elements (Figures/Charts/Diagrams)
|
|
6980
|
+
|
|
6981
|
+
Check if images were extracted alongside the upload:
|
|
6982
|
+
```bash
|
|
6983
|
+
bash: ls uploaded/<filename_stem>*images* 2>/dev/null || ls uploaded/*images*/ 2>/dev/null
|
|
6984
|
+
```
|
|
6985
|
+
|
|
6986
|
+
If image directory exists:
|
|
6987
|
+
1. `read_file` on each image file — runtime injects as native vision input
|
|
6988
|
+
2. Correlate each image with nearby text/captions in the parsed content
|
|
6989
|
+
3. Describe what the figure shows and how it relates to the text
|
|
6990
|
+
|
|
6991
|
+
If no images extracted but the PDF contains figures:
|
|
6992
|
+
```bash
|
|
6993
|
+
# Probe PyMuPDF availability
|
|
6994
|
+
bash: python3 -c "import fitz; print('PyMuPDF available')" 2>&1
|
|
6995
|
+
# If available, extract figures
|
|
6996
|
+
bash: python3 -c "
|
|
6997
|
+
import fitz
|
|
6998
|
+
doc = fitz.open('uploaded/<filename>.pdf')
|
|
6999
|
+
for i, page in enumerate(doc):
|
|
7000
|
+
for j, img in enumerate(page.get_images(full=True)):
|
|
7001
|
+
xref = img[0]
|
|
7002
|
+
pix = fitz.Pixmap(doc, xref)
|
|
7003
|
+
if pix.n >= 5: pix = fitz.Pixmap(fitz.csRGB, pix)
|
|
7004
|
+
pix.save(f'uploaded/fig_p{i+1}_{j+1}.png')
|
|
7005
|
+
print(f'Saved: uploaded/fig_p{i+1}_{j+1}.png')
|
|
7006
|
+
"
|
|
7007
|
+
```
|
|
7008
|
+
|
|
7009
|
+
## Step 4: Multi-PDF Comparison
|
|
7010
|
+
|
|
7011
|
+
When comparing multiple PDFs:
|
|
7012
|
+
1. Read each PDF's `.parsed.md`
|
|
7013
|
+
2. Build a comparison matrix:
|
|
7014
|
+
|
|
7015
|
+
| Aspect | Document A | Document B | Notes |
|
|
7016
|
+
|--------|-----------|-----------|-------|
|
|
7017
|
+
| Topic | ... | ... | Agreement/Difference |
|
|
7018
|
+
| Method | ... | ... | ... |
|
|
7019
|
+
| Findings | ... | ... | ... |
|
|
7020
|
+
|
|
7021
|
+
3. Synthesize: What do the documents agree on? Where do they differ? Why?
|
|
7022
|
+
|
|
7023
|
+
## Step 5: Summary Generation
|
|
7024
|
+
|
|
7025
|
+
Three depth levels:
|
|
7026
|
+
|
|
7027
|
+
### Executive Summary (3-5 bullets)
|
|
7028
|
+
- Core conclusion
|
|
7029
|
+
- Key data point
|
|
7030
|
+
- Main recommendation
|
|
7031
|
+
- Critical limitation
|
|
7032
|
+
|
|
7033
|
+
### Standard Summary (section-by-section)
|
|
7034
|
+
For each major section: 2-3 sentences covering the key points.
|
|
7035
|
+
Always cite section/page numbers.
|
|
7036
|
+
|
|
7037
|
+
### Deep Summary (claim-evidence-source)
|
|
7038
|
+
| Claim | Evidence | Source (page/section) | Confidence |
|
|
7039
|
+
|-------|----------|----------------------|------------|
|
|
7040
|
+
|
|
7041
|
+
## Step 6: Fallback for Scanned PDFs
|
|
7042
|
+
|
|
7043
|
+
If text extraction returns empty/garbled content:
|
|
7044
|
+
```bash
|
|
7045
|
+
# OCR with tesseract
|
|
7046
|
+
bash: tesseract "uploaded/<filename>.pdf" stdout pdf 2>/dev/null || echo "tesseract not available"
|
|
7047
|
+
|
|
7048
|
+
# ocrmypdf (adds OCR layer to PDF, then re-extract)
|
|
7049
|
+
bash: ocrmypdf "uploaded/<filename>.pdf" "/tmp/ocr_output.pdf" && pdftotext "/tmp/ocr_output.pdf" -
|
|
7050
|
+
```
|
|
7051
|
+
|
|
7052
|
+
## Notes
|
|
7053
|
+
- Always cite page numbers or section references in your analysis
|
|
7054
|
+
- For very large PDFs (100+ pages), focus on the sections relevant to the user's question
|
|
7055
|
+
- When RAG library has the PDF imported, also try: `query_knowledge_library(query="<topic>", route="hybrid")`
|
|
7056
|
+
"""
|
|
7057
|
+
|
|
7058
|
+
# ── Skill D: audio-comprehension ──────────────────────────────────
|
|
7059
|
+
|
|
7060
|
+
audio_skill = """---
|
|
7061
|
+
name: audio-comprehension
|
|
7062
|
+
aliases:
|
|
7063
|
+
- audio-reader
|
|
7064
|
+
- audio-analysis
|
|
7065
|
+
- listen-audio
|
|
7066
|
+
- transcription
|
|
7067
|
+
triggers:
|
|
7068
|
+
- audio analysis
|
|
7069
|
+
- listen to audio
|
|
7070
|
+
- transcribe
|
|
7071
|
+
- meeting notes
|
|
7072
|
+
- lecture notes
|
|
7073
|
+
- podcast summary
|
|
7074
|
+
- speech analysis
|
|
7075
|
+
- 音频分析
|
|
7076
|
+
- 听音频
|
|
7077
|
+
- 转录
|
|
7078
|
+
- 会议纪要
|
|
7079
|
+
- 讲座笔记
|
|
7080
|
+
- 语音识别
|
|
7081
|
+
clouds_coder:
|
|
7082
|
+
preferred_tools:
|
|
7083
|
+
- read_file
|
|
7084
|
+
- bash
|
|
7085
|
+
description: >
|
|
7086
|
+
Audio comprehension workflow: native analysis via read_file when model supports audio input,
|
|
7087
|
+
with whisper/ffmpeg fallback. Covers speech, music, sound analysis, meeting/lecture notes.
|
|
7088
|
+
TRIGGER when: user uploads audio and asks for analysis/transcription/notes/summary.
|
|
7089
|
+
DO NOT TRIGGER for: audio generation or text-to-speech tasks.
|
|
7090
|
+
---
|
|
7091
|
+
|
|
7092
|
+
# Audio Comprehension
|
|
7093
|
+
|
|
7094
|
+
Workflow for analyzing and understanding audio files.
|
|
7095
|
+
|
|
7096
|
+
## Supported Formats
|
|
7097
|
+
mp3, wav, m4a, aac, flac, ogg, oga, opus, webm
|
|
7098
|
+
|
|
7099
|
+
## Primary: Native Audio Analysis
|
|
7100
|
+
|
|
7101
|
+
Use `read_file` on the audio file — the runtime automatically injects it as native audio input
|
|
7102
|
+
if the model supports it:
|
|
7103
|
+
```
|
|
7104
|
+
read_file uploaded/<filename>.mp3
|
|
7105
|
+
```
|
|
7106
|
+
|
|
7107
|
+
The model can then directly analyze:
|
|
7108
|
+
- Speech content (what is being said)
|
|
7109
|
+
- Speaker tone, emotion, emphasis
|
|
7110
|
+
- Music elements (if applicable)
|
|
7111
|
+
- Background sounds and environment
|
|
7112
|
+
|
|
7113
|
+
**After loading**, describe what you hear and answer the user's question based on the audio content.
|
|
7114
|
+
|
|
7115
|
+
## Fallback: Transcription Tools
|
|
7116
|
+
|
|
7117
|
+
If the model reports native audio input is unavailable, use external transcription:
|
|
7118
|
+
|
|
7119
|
+
### Whisper (recommended)
|
|
7120
|
+
```bash
|
|
7121
|
+
# Quick transcription
|
|
7122
|
+
bash: whisper "uploaded/<filename>.mp3" --model base --output_format txt --output_dir /tmp/
|
|
7123
|
+
|
|
7124
|
+
# Higher quality (slower)
|
|
7125
|
+
bash: whisper "uploaded/<filename>.mp3" --model small --output_format txt --output_dir /tmp/
|
|
7126
|
+
|
|
7127
|
+
# Read transcript
|
|
7128
|
+
read_file /tmp/<filename>.txt
|
|
7129
|
+
```
|
|
7130
|
+
|
|
7131
|
+
### Python whisper library
|
|
7132
|
+
```bash
|
|
7133
|
+
bash: python3 -c "
|
|
7134
|
+
import whisper
|
|
7135
|
+
model = whisper.load_model('base')
|
|
7136
|
+
result = model.transcribe('uploaded/<filename>.mp3')
|
|
7137
|
+
print(result['text'])
|
|
7138
|
+
"
|
|
7139
|
+
```
|
|
7140
|
+
|
|
7141
|
+
### Format conversion (if needed)
|
|
7142
|
+
```bash
|
|
7143
|
+
# Convert to wav for better compatibility
|
|
7144
|
+
bash: ffmpeg -i "uploaded/<filename>.m4a" -ar 16000 -ac 1 /tmp/converted.wav
|
|
7145
|
+
```
|
|
7146
|
+
|
|
7147
|
+
## Meeting Notes Workflow
|
|
7148
|
+
|
|
7149
|
+
1. Load audio via `read_file` (or transcribe via fallback)
|
|
7150
|
+
2. Identify key elements:
|
|
7151
|
+
- Participants (if identifiable)
|
|
7152
|
+
- Topics discussed (segment by theme)
|
|
7153
|
+
- Decisions made
|
|
7154
|
+
- Action items assigned
|
|
7155
|
+
3. Output structured meeting notes:
|
|
7156
|
+
|
|
7157
|
+
```markdown
|
|
7158
|
+
# Meeting Notes — [Date/Topic]
|
|
7159
|
+
|
|
7160
|
+
## Participants
|
|
7161
|
+
- Speaker A, Speaker B, ...
|
|
7162
|
+
|
|
7163
|
+
## Discussion Topics
|
|
7164
|
+
### Topic 1: [name]
|
|
7165
|
+
- Key points discussed
|
|
7166
|
+
- Decisions: ...
|
|
7167
|
+
|
|
7168
|
+
### Topic 2: [name]
|
|
7169
|
+
- ...
|
|
7170
|
+
|
|
7171
|
+
## Action Items
|
|
7172
|
+
- [ ] [Person]: [Action] — by [deadline]
|
|
7173
|
+
|
|
7174
|
+
## Follow-up
|
|
7175
|
+
- Next meeting: ...
|
|
7176
|
+
```
|
|
7177
|
+
|
|
7178
|
+
## Lecture / Podcast Summary
|
|
7179
|
+
|
|
7180
|
+
1. Load or transcribe the audio
|
|
7181
|
+
2. Identify the structure:
|
|
7182
|
+
- Introduction / topic statement
|
|
7183
|
+
- Main arguments / sections
|
|
7184
|
+
- Examples and evidence
|
|
7185
|
+
- Conclusion / takeaways
|
|
7186
|
+
3. Output:
|
|
7187
|
+
- **Key Topics** (bulleted list)
|
|
7188
|
+
- **Section-by-section summary** (2-3 sentences each)
|
|
7189
|
+
- **Notable quotes** (verbatim if possible)
|
|
7190
|
+
- **Study questions** (for lectures)
|
|
7191
|
+
|
|
7192
|
+
## Quality Notes
|
|
7193
|
+
- File size limit: <20 MB for native audio input
|
|
7194
|
+
- Long audio (>15 min): Consider chunking via ffmpeg:
|
|
7195
|
+
`bash: ffmpeg -i input.mp3 -ss 0 -t 300 chunk1.mp3` (first 5 min)
|
|
7196
|
+
- Noisy audio: Transcription quality degrades — note uncertainty in output
|
|
7197
|
+
- Multiple speakers: Native models may distinguish speakers; whisper may not
|
|
7198
|
+
"""
|
|
7199
|
+
|
|
7200
|
+
# ── Skill E: video-comprehension ──────────────────────────────────
|
|
7201
|
+
|
|
7202
|
+
video_skill = """---
|
|
7203
|
+
name: video-comprehension
|
|
7204
|
+
aliases:
|
|
7205
|
+
- video-reader
|
|
7206
|
+
- video-analysis
|
|
7207
|
+
- watch-video
|
|
7208
|
+
triggers:
|
|
7209
|
+
- video analysis
|
|
7210
|
+
- watch video
|
|
7211
|
+
- video summary
|
|
7212
|
+
- video transcript
|
|
7213
|
+
- screen recording
|
|
7214
|
+
- 视频分析
|
|
7215
|
+
- 看视频
|
|
7216
|
+
- 视频摘要
|
|
7217
|
+
- 视频转录
|
|
7218
|
+
- 录屏分析
|
|
7219
|
+
clouds_coder:
|
|
7220
|
+
preferred_tools:
|
|
7221
|
+
- read_file
|
|
7222
|
+
- bash
|
|
7223
|
+
description: >
|
|
7224
|
+
Video comprehension workflow: native analysis via read_file when model supports video input,
|
|
7225
|
+
with frame extraction fallback via ffmpeg. Covers temporal analysis, video summarization,
|
|
7226
|
+
screen recording interpretation, and tutorial extraction.
|
|
7227
|
+
TRIGGER when: user uploads video and asks for analysis/summary/transcription.
|
|
7228
|
+
DO NOT TRIGGER for: video generation tasks.
|
|
7229
|
+
---
|
|
7230
|
+
|
|
7231
|
+
# Video Comprehension
|
|
7232
|
+
|
|
7233
|
+
Workflow for analyzing and understanding video files.
|
|
7234
|
+
|
|
7235
|
+
## Supported Formats
|
|
7236
|
+
mp4, mov, avi, mkv, webm, m4v, mpeg, mpg, 3gp
|
|
7237
|
+
|
|
7238
|
+
## Primary: Native Video Analysis
|
|
7239
|
+
|
|
7240
|
+
Use `read_file` on the video file — the runtime injects it as native video input if the model supports it:
|
|
7241
|
+
```
|
|
7242
|
+
read_file uploaded/<filename>.mp4
|
|
7243
|
+
```
|
|
7244
|
+
|
|
7245
|
+
The model can then directly analyze:
|
|
7246
|
+
- Visual content (scenes, objects, people, text overlays)
|
|
7247
|
+
- Audio track (speech, music, sound effects)
|
|
7248
|
+
- Temporal flow (what happens when)
|
|
7249
|
+
- UI elements (for screen recordings)
|
|
7250
|
+
|
|
7251
|
+
## Fallback: Frame Extraction + Audio Separation
|
|
7252
|
+
|
|
7253
|
+
If native video input is unavailable:
|
|
7254
|
+
|
|
7255
|
+
### Extract Key Frames
|
|
7256
|
+
```bash
|
|
7257
|
+
# One frame every 5 seconds
|
|
7258
|
+
bash: mkdir -p /tmp/frames && ffmpeg -i "uploaded/<filename>.mp4" -vf "fps=1/5" -q:v 2 /tmp/frames/frame_%04d.jpg 2>&1 | tail -3
|
|
7259
|
+
|
|
7260
|
+
# List extracted frames
|
|
7261
|
+
bash: ls /tmp/frames/
|
|
7262
|
+
```
|
|
7263
|
+
|
|
7264
|
+
Then analyze each frame via `read_file`:
|
|
7265
|
+
```
|
|
7266
|
+
read_file /tmp/frames/frame_0001.jpg
|
|
7267
|
+
read_file /tmp/frames/frame_0002.jpg
|
|
7268
|
+
...
|
|
7269
|
+
```
|
|
7270
|
+
|
|
7271
|
+
### Extract Audio Track
|
|
7272
|
+
```bash
|
|
7273
|
+
bash: ffmpeg -i "uploaded/<filename>.mp4" -vn -acodec pcm_s16le /tmp/audio_track.wav 2>&1 | tail -3
|
|
7274
|
+
```
|
|
7275
|
+
Then apply the `audio-comprehension` workflow on the extracted audio.
|
|
7276
|
+
|
|
7277
|
+
### Combine Visual + Audio Analysis
|
|
7278
|
+
Merge the visual scene descriptions with audio transcription to build a complete understanding.
|
|
7279
|
+
|
|
7280
|
+
## Screen Recording Analysis
|
|
7281
|
+
|
|
7282
|
+
For screen recordings / UI walkthroughs:
|
|
7283
|
+
1. Extract frames at higher frequency (every 2 seconds):
|
|
7284
|
+
`bash: ffmpeg -i input.mp4 -vf "fps=1/2" frames/frame_%04d.jpg`
|
|
7285
|
+
2. Analyze each frame for:
|
|
7286
|
+
- Application/webpage being shown
|
|
7287
|
+
- UI state changes between frames
|
|
7288
|
+
- Text content on screen
|
|
7289
|
+
- Error messages or notifications
|
|
7290
|
+
3. Reconstruct the workflow step by step
|
|
7291
|
+
4. Output as a tutorial or issue report
|
|
7292
|
+
|
|
7293
|
+
## Video Summarization
|
|
7294
|
+
|
|
7295
|
+
1. Load video natively or extract frames
|
|
7296
|
+
2. Build scene list:
|
|
7297
|
+
| Timestamp | Scene Description | Key Content |
|
|
7298
|
+
|-----------|------------------|-------------|
|
|
7299
|
+
| 0:00-0:30 | Intro | Title, speaker |
|
|
7300
|
+
| 0:30-2:00 | Topic 1 | Main argument |
|
|
7301
|
+
| ... | ... | ... |
|
|
7302
|
+
3. Generate structured summary:
|
|
7303
|
+
- **Overview** (1-2 sentences)
|
|
7304
|
+
- **Timeline** (scene-by-scene)
|
|
7305
|
+
- **Key moments** (with timestamps)
|
|
7306
|
+
- **Conclusions**
|
|
7307
|
+
|
|
7308
|
+
## Quality Notes
|
|
7309
|
+
- File size limit: <20 MB for native video input
|
|
7310
|
+
- Long videos: Sample frames strategically rather than extracting all
|
|
7311
|
+
- First frame, last frame, and N evenly-spaced frames in between
|
|
7312
|
+
- Resolution affects analysis quality — high-res frames give better results
|
|
7313
|
+
- For tutorials: higher frame rate (1 fps) captures more UI state changes
|
|
7314
|
+
"""
|
|
7315
|
+
|
|
7316
|
+
# ── Skill F: data-analysis-deep-dive ──────────────────────────────
|
|
7317
|
+
|
|
7318
|
+
data_skill = """---
|
|
7319
|
+
name: data-analysis-deep-dive
|
|
7320
|
+
aliases:
|
|
7321
|
+
- data-deep-dive
|
|
7322
|
+
- deep-data-analysis
|
|
7323
|
+
- advanced-tabular
|
|
7324
|
+
- data-science
|
|
7325
|
+
triggers:
|
|
7326
|
+
- data analysis
|
|
7327
|
+
- statistical analysis
|
|
7328
|
+
- data profiling
|
|
7329
|
+
- data visualization
|
|
7330
|
+
- dataset comparison
|
|
7331
|
+
- data quality
|
|
7332
|
+
- correlation analysis
|
|
7333
|
+
- 数据分析
|
|
7334
|
+
- 统计分析
|
|
7335
|
+
- 数据质量
|
|
7336
|
+
- 数据可视化
|
|
7337
|
+
- 数据对比
|
|
7338
|
+
- 相关性分析
|
|
7339
|
+
clouds_coder:
|
|
7340
|
+
preferred_tools:
|
|
7341
|
+
- bash
|
|
7342
|
+
- read_file
|
|
7343
|
+
- write_file
|
|
7344
|
+
- query_knowledge_library
|
|
7345
|
+
description: >
|
|
7346
|
+
Enhanced data analysis skill for CSV/XLSX files. Statistical analysis via Python,
|
|
7347
|
+
data profiling, visualization generation, quality assessment, cross-dataset comparison,
|
|
7348
|
+
and RAG-backed contextual analysis.
|
|
7349
|
+
TRIGGER when: user needs statistical analysis, data profiling, visualization, or dataset comparison.
|
|
7350
|
+
DO NOT TRIGGER for: simple file reading (use upload-tabular-parser),
|
|
7351
|
+
research workflows (use research-orchestrator-pro).
|
|
7352
|
+
---
|
|
7353
|
+
|
|
7354
|
+
# Data Analysis Deep Dive
|
|
7355
|
+
|
|
7356
|
+
Advanced data analysis workflow beyond basic file parsing.
|
|
7357
|
+
|
|
7358
|
+
## When to Use
|
|
7359
|
+
- Statistical analysis, hypothesis testing
|
|
7360
|
+
- Data profiling and quality assessment
|
|
7361
|
+
- Visualization / chart generation
|
|
7362
|
+
- Cross-dataset comparison
|
|
7363
|
+
- RAG-backed contextual analysis
|
|
7364
|
+
|
|
7365
|
+
## When NOT to Use
|
|
7366
|
+
- Just reading a CSV/XLSX file → use `upload-tabular-parser`
|
|
7367
|
+
- Full research workflow → use `research-orchestrator-pro` Phase 2
|
|
7368
|
+
|
|
7369
|
+
## Step 1: Data Ingestion
|
|
7370
|
+
|
|
7371
|
+
Read the `.parsed.md` for a quick preview, then load properly via Python:
|
|
7372
|
+
```bash
|
|
7373
|
+
bash: python3 -c "
|
|
7374
|
+
import pandas as pd
|
|
7375
|
+
df = pd.read_csv('uploaded/<filename>.csv')
|
|
7376
|
+
print('Shape:', df.shape)
|
|
7377
|
+
print('Columns:', list(df.columns))
|
|
7378
|
+
print('Types:', df.dtypes.to_dict())
|
|
7379
|
+
print('Head:')
|
|
7380
|
+
print(df.head())
|
|
7381
|
+
"
|
|
7382
|
+
```
|
|
7383
|
+
|
|
7384
|
+
For XLSX:
|
|
7385
|
+
```bash
|
|
7386
|
+
bash: python3 -c "
|
|
7387
|
+
import pandas as pd
|
|
7388
|
+
xls = pd.ExcelFile('uploaded/<filename>.xlsx')
|
|
7389
|
+
print('Sheets:', xls.sheet_names)
|
|
7390
|
+
df = pd.read_excel(xls, sheet_name=0)
|
|
7391
|
+
print('Shape:', df.shape)
|
|
7392
|
+
print('Head:')
|
|
7393
|
+
print(df.head())
|
|
7394
|
+
"
|
|
7395
|
+
```
|
|
7396
|
+
|
|
7397
|
+
Handle encoding/delimiter issues:
|
|
7398
|
+
```bash
|
|
7399
|
+
# Auto-detect encoding
|
|
7400
|
+
bash: python3 -c "
|
|
7401
|
+
import chardet
|
|
7402
|
+
with open('uploaded/<filename>.csv', 'rb') as f:
|
|
7403
|
+
result = chardet.detect(f.read(10000))
|
|
7404
|
+
print(result)
|
|
7405
|
+
"
|
|
7406
|
+
```
|
|
7407
|
+
|
|
7408
|
+
## Step 2: Data Profiling
|
|
7409
|
+
|
|
7410
|
+
Quick automated profile:
|
|
7411
|
+
```bash
|
|
7412
|
+
bash: python3 -c "
|
|
7413
|
+
import pandas as pd
|
|
7414
|
+
df = pd.read_csv('uploaded/<filename>.csv')
|
|
7415
|
+
print('=== Shape ===')
|
|
7416
|
+
print(df.shape)
|
|
7417
|
+
print('=== Missing Values ===')
|
|
7418
|
+
print(df.isnull().sum())
|
|
7419
|
+
print('=== Unique Counts ===')
|
|
7420
|
+
print(df.nunique())
|
|
7421
|
+
print('=== Descriptive Stats ===')
|
|
7422
|
+
print(df.describe(include='all').to_string())
|
|
7423
|
+
"
|
|
7424
|
+
```
|
|
7425
|
+
|
|
7426
|
+
Key metrics to report:
|
|
7427
|
+
- Row count, column count
|
|
7428
|
+
- Missing values per column (count and percentage)
|
|
7429
|
+
- Unique values per column
|
|
7430
|
+
- Min/max/mean/std for numeric columns
|
|
7431
|
+
- Top-N value counts for categorical columns
|
|
7432
|
+
|
|
7433
|
+
## Step 3: Statistical Analysis
|
|
7434
|
+
|
|
7435
|
+
Choose the right analysis based on the question:
|
|
7436
|
+
|
|
7437
|
+
### Descriptive Statistics
|
|
7438
|
+
```python
|
|
7439
|
+
df.describe(), df.corr(), df.groupby('category').mean()
|
|
7440
|
+
```
|
|
7441
|
+
|
|
7442
|
+
### Correlation Analysis
|
|
7443
|
+
```bash
|
|
7444
|
+
bash: python3 -c "
|
|
7445
|
+
import pandas as pd
|
|
7446
|
+
df = pd.read_csv('uploaded/<filename>.csv')
|
|
7447
|
+
corr = df.select_dtypes(include='number').corr()
|
|
7448
|
+
print(corr.to_string())
|
|
7449
|
+
# Highlight strong correlations
|
|
7450
|
+
for col1 in corr.columns:
|
|
7451
|
+
for col2 in corr.columns:
|
|
7452
|
+
if col1 < col2 and abs(corr.loc[col1, col2]) > 0.7:
|
|
7453
|
+
print(f'Strong: {col1} <-> {col2}: {corr.loc[col1, col2]:.3f}')
|
|
7454
|
+
"
|
|
7455
|
+
```
|
|
7456
|
+
|
|
7457
|
+
### Hypothesis Testing
|
|
7458
|
+
```bash
|
|
7459
|
+
bash: python3 -c "
|
|
7460
|
+
from scipy import stats
|
|
7461
|
+
import pandas as pd
|
|
7462
|
+
df = pd.read_csv('uploaded/<filename>.csv')
|
|
7463
|
+
# Example: t-test between two groups
|
|
7464
|
+
group_a = df[df['group'] == 'A']['value']
|
|
7465
|
+
group_b = df[df['group'] == 'B']['value']
|
|
7466
|
+
t_stat, p_value = stats.ttest_ind(group_a, group_b)
|
|
7467
|
+
print(f't-stat: {t_stat:.4f}, p-value: {p_value:.4f}')
|
|
7468
|
+
print('Significant' if p_value < 0.05 else 'Not significant')
|
|
7469
|
+
"
|
|
7470
|
+
```
|
|
7471
|
+
|
|
7472
|
+
## Step 4: Visualization
|
|
7473
|
+
|
|
7474
|
+
Generate charts and save to workspace:
|
|
7475
|
+
|
|
7476
|
+
### Bar Chart (comparison)
|
|
7477
|
+
```bash
|
|
7478
|
+
bash: python3 -c "
|
|
7479
|
+
import matplotlib
|
|
7480
|
+
matplotlib.use('Agg')
|
|
7481
|
+
import matplotlib.pyplot as plt
|
|
7482
|
+
import pandas as pd
|
|
7483
|
+
df = pd.read_csv('uploaded/<filename>.csv')
|
|
7484
|
+
df.groupby('category')['value'].mean().plot(kind='bar', title='Average Value by Category')
|
|
7485
|
+
plt.ylabel('Value')
|
|
7486
|
+
plt.tight_layout()
|
|
7487
|
+
plt.savefig('chart_comparison.png', dpi=150)
|
|
7488
|
+
print('Saved: chart_comparison.png')
|
|
7489
|
+
"
|
|
7490
|
+
```
|
|
7491
|
+
|
|
7492
|
+
### Line Chart (trend)
|
|
7493
|
+
```bash
|
|
7494
|
+
bash: python3 -c "
|
|
7495
|
+
import matplotlib; matplotlib.use('Agg')
|
|
7496
|
+
import matplotlib.pyplot as plt; import pandas as pd
|
|
7497
|
+
df = pd.read_csv('uploaded/<filename>.csv')
|
|
7498
|
+
plt.figure(figsize=(10,5))
|
|
7499
|
+
plt.plot(df['date'], df['value'])
|
|
7500
|
+
plt.title('Value Trend'); plt.xlabel('Date'); plt.ylabel('Value')
|
|
7501
|
+
plt.xticks(rotation=45); plt.tight_layout()
|
|
7502
|
+
plt.savefig('chart_trend.png', dpi=150); print('Saved: chart_trend.png')
|
|
7503
|
+
"
|
|
7504
|
+
```
|
|
7505
|
+
|
|
7506
|
+
### Chart Selection Guide
|
|
7507
|
+
| Intent | Chart Type | matplotlib call |
|
|
7508
|
+
|--------|-----------|-----------------|
|
|
7509
|
+
| Comparison | Bar | `.plot(kind='bar')` |
|
|
7510
|
+
| Trend over time | Line | `plt.plot()` |
|
|
7511
|
+
| Distribution | Histogram | `.plot(kind='hist')` |
|
|
7512
|
+
| Correlation | Scatter | `plt.scatter()` |
|
|
7513
|
+
| Composition | Pie | `.plot(kind='pie')` |
|
|
7514
|
+
| Distribution shape | Box plot | `.plot(kind='box')` |
|
|
7515
|
+
|
|
7516
|
+
Always include: title, axis labels, legend (if multiple series), tight_layout.
|
|
7517
|
+
|
|
7518
|
+
## Step 5: Data Quality Assessment
|
|
7519
|
+
|
|
7520
|
+
```bash
|
|
7521
|
+
bash: python3 -c "
|
|
7522
|
+
import pandas as pd
|
|
7523
|
+
df = pd.read_csv('uploaded/<filename>.csv')
|
|
7524
|
+
print('=== Missing Data ===')
|
|
7525
|
+
missing = df.isnull().sum()
|
|
7526
|
+
missing_pct = (missing / len(df) * 100).round(2)
|
|
7527
|
+
print(pd.DataFrame({'count': missing, 'percent': missing_pct})[missing > 0].to_string())
|
|
7528
|
+
|
|
7529
|
+
print('\\n=== Duplicates ===')
|
|
7530
|
+
print(f'Duplicate rows: {df.duplicated().sum()} / {len(df)}')
|
|
7531
|
+
|
|
7532
|
+
print('\\n=== Outliers (IQR method) ===')
|
|
7533
|
+
for col in df.select_dtypes(include='number').columns:
|
|
7534
|
+
Q1, Q3 = df[col].quantile(0.25), df[col].quantile(0.75)
|
|
7535
|
+
IQR = Q3 - Q1
|
|
7536
|
+
outliers = ((df[col] < Q1 - 1.5*IQR) | (df[col] > Q3 + 1.5*IQR)).sum()
|
|
7537
|
+
if outliers > 0:
|
|
7538
|
+
print(f' {col}: {outliers} outliers')
|
|
7539
|
+
"
|
|
7540
|
+
```
|
|
7541
|
+
|
|
7542
|
+
## Step 6: Cross-Dataset Comparison
|
|
7543
|
+
|
|
7544
|
+
When comparing two datasets:
|
|
7545
|
+
1. Load both: `df_a = pd.read_csv('a.csv')`, `df_b = pd.read_csv('b.csv')`
|
|
7546
|
+
2. Compare schemas: column names, types, missing patterns
|
|
7547
|
+
3. Compare distributions: mean, std, min, max for shared columns
|
|
7548
|
+
4. Identify drift: significant differences in distributions
|
|
7549
|
+
5. Output comparison matrix
|
|
7550
|
+
|
|
7551
|
+
## RAG Context Integration
|
|
7552
|
+
|
|
7553
|
+
Use `query_knowledge_library` to ground your analysis in domain knowledge:
|
|
7554
|
+
```
|
|
7555
|
+
query_knowledge_library(query="normal range for <metric>", route="hybrid")
|
|
7556
|
+
query_knowledge_library(query="<domain> benchmark data", route="hybrid")
|
|
7557
|
+
```
|
|
7558
|
+
|
|
7559
|
+
This helps you:
|
|
7560
|
+
- Know what ranges are "normal" for the domain
|
|
7561
|
+
- Compare against published benchmarks
|
|
7562
|
+
- Understand what statistical tests are appropriate
|
|
7563
|
+
"""
|
|
7564
|
+
|
|
7565
|
+
_write_text_if_changed(generated_root / "pdf-reading-comprehension" / "SKILL.md", pdf_skill)
|
|
7566
|
+
_write_text_if_changed(generated_root / "audio-comprehension" / "SKILL.md", audio_skill)
|
|
7567
|
+
_write_text_if_changed(generated_root / "video-comprehension" / "SKILL.md", video_skill)
|
|
7568
|
+
_write_text_if_changed(generated_root / "data-analysis-deep-dive" / "SKILL.md", data_skill)
|
|
7569
|
+
_write_text_if_changed(
|
|
7570
|
+
generated_root / "multimodal-comprehension-capabilities.json",
|
|
7571
|
+
json_dumps(
|
|
7572
|
+
{
|
|
7573
|
+
"generated_at": int(now_ts()),
|
|
7574
|
+
"skills": [
|
|
7575
|
+
"pdf-reading-comprehension",
|
|
7576
|
+
"audio-comprehension",
|
|
7577
|
+
"video-comprehension",
|
|
7578
|
+
"data-analysis-deep-dive",
|
|
7579
|
+
],
|
|
7580
|
+
"focus": [
|
|
7581
|
+
"pdf_comprehension",
|
|
7582
|
+
"audio_comprehension",
|
|
7583
|
+
"video_comprehension",
|
|
7584
|
+
"advanced_data_analysis",
|
|
7585
|
+
],
|
|
7586
|
+
},
|
|
7587
|
+
indent=2,
|
|
7588
|
+
),
|
|
7589
|
+
)
|
|
7590
|
+
|
|
7591
|
+
|
|
6419
7592
|
def ensure_generated_runtime_skills_manifest(skills_root: Path):
|
|
6420
7593
|
generated_root = skills_root / "generated"
|
|
6421
7594
|
tracked = [
|
|
@@ -6746,6 +7919,8 @@ def ensure_runtime_skills(skills_root: Path):
|
|
|
6746
7919
|
ensure_generated_html_frontend_report_skills(skills_root)
|
|
6747
7920
|
ensure_generated_deep_research_skills(skills_root)
|
|
6748
7921
|
ensure_generated_research_scientific_skills(skills_root)
|
|
7922
|
+
ensure_generated_rag_mastery_skills(skills_root)
|
|
7923
|
+
ensure_generated_multimodal_comprehension_skills(skills_root)
|
|
6749
7924
|
ensure_generated_runtime_skills_manifest(skills_root)
|
|
6750
7925
|
ensure_embedded_clawhub_skills(skills_root)
|
|
6751
7926
|
|
|
@@ -7153,6 +8328,117 @@ class SkillStore:
|
|
|
7153
8328
|
total_chars += len(text)
|
|
7154
8329
|
return chosen
|
|
7155
8330
|
|
|
8331
|
+
def _provider_root_path(self, provider_id: str) -> Path | None:
|
|
8332
|
+
row = self.providers.get(str(provider_id or "").strip(), {})
|
|
8333
|
+
raw = str(row.get("root", "") or "").strip() if isinstance(row, dict) else ""
|
|
8334
|
+
if not raw:
|
|
8335
|
+
return None
|
|
8336
|
+
try:
|
|
8337
|
+
return Path(raw).resolve()
|
|
8338
|
+
except Exception:
|
|
8339
|
+
return None
|
|
8340
|
+
|
|
8341
|
+
def _provider_virtual_prefix(self, provider_id: str) -> str:
|
|
8342
|
+
pid = self._sanitize_provider_id(provider_id, "provider")
|
|
8343
|
+
return f"{SKILLS_VIRTUAL_PREFIX}/{SKILLS_EXTERNAL_MOUNT}/{pid}"
|
|
8344
|
+
|
|
8345
|
+
def _public_provider_path(self, provider_id: str, raw_locator: str | Path) -> str:
|
|
8346
|
+
raw = str(raw_locator or "").strip()
|
|
8347
|
+
if not raw:
|
|
8348
|
+
return ""
|
|
8349
|
+
if raw == "(builtin)" or _is_http_url(raw):
|
|
8350
|
+
return raw
|
|
8351
|
+
base, sep, frag = raw.partition("#")
|
|
8352
|
+
if _is_http_url(base):
|
|
8353
|
+
return raw
|
|
8354
|
+
try:
|
|
8355
|
+
candidate = Path(base)
|
|
8356
|
+
except Exception:
|
|
8357
|
+
return self._public_skill_locator(raw)
|
|
8358
|
+
try:
|
|
8359
|
+
resolved = candidate.resolve() if candidate.is_absolute() else (self.skills_root / candidate).resolve()
|
|
8360
|
+
except Exception:
|
|
8361
|
+
resolved = None
|
|
8362
|
+
if resolved is not None:
|
|
8363
|
+
try:
|
|
8364
|
+
rel = resolved.relative_to(self.skills_root.resolve()).as_posix()
|
|
8365
|
+
public = SKILLS_VIRTUAL_PREFIX if rel in {"", "."} else f"{SKILLS_VIRTUAL_PREFIX}/{rel}"
|
|
8366
|
+
public = public.replace("//", "/")
|
|
8367
|
+
return public + (f"#{frag}" if sep else "")
|
|
8368
|
+
except Exception:
|
|
8369
|
+
pass
|
|
8370
|
+
root = self._provider_root_path(provider_id)
|
|
8371
|
+
if root is not None:
|
|
8372
|
+
try:
|
|
8373
|
+
rel = resolved.relative_to(root).as_posix()
|
|
8374
|
+
prefix = self._provider_virtual_prefix(provider_id)
|
|
8375
|
+
public = prefix if rel in {"", "."} else f"{prefix}/{rel}"
|
|
8376
|
+
public = public.replace("//", "/")
|
|
8377
|
+
return public + (f"#{frag}" if sep else "")
|
|
8378
|
+
except Exception:
|
|
8379
|
+
pass
|
|
8380
|
+
return self._public_skill_locator(raw)
|
|
8381
|
+
|
|
8382
|
+
def resolve_virtual_skill_path(self, rel_path: str) -> Path:
|
|
8383
|
+
rel = str(PurePosixPath(str(rel_path or "").replace("\\", "/"))).strip().lstrip("/")
|
|
8384
|
+
parts = PurePosixPath(rel).parts
|
|
8385
|
+
if len(parts) >= 2 and parts[0] == SKILLS_EXTERNAL_MOUNT:
|
|
8386
|
+
provider_id = str(parts[1]).strip()
|
|
8387
|
+
root = self._provider_root_path(provider_id)
|
|
8388
|
+
if root is None:
|
|
8389
|
+
raise ValueError(f"unknown external skill provider: {provider_id}")
|
|
8390
|
+
tail = PurePosixPath(*parts[2:]).as_posix() if len(parts) > 2 else "."
|
|
8391
|
+
return safe_path(tail or ".", root)
|
|
8392
|
+
return safe_path(rel or ".", self.skills_root)
|
|
8393
|
+
|
|
8394
|
+
def public_virtual_skill_path_for_abs(self, path: Path) -> str:
|
|
8395
|
+
try:
|
|
8396
|
+
resolved = path.resolve()
|
|
8397
|
+
except Exception:
|
|
8398
|
+
return ""
|
|
8399
|
+
try:
|
|
8400
|
+
rel = resolved.relative_to(self.skills_root.resolve()).as_posix()
|
|
8401
|
+
public = SKILLS_VIRTUAL_PREFIX if rel in {"", "."} else f"{SKILLS_VIRTUAL_PREFIX}/{rel}"
|
|
8402
|
+
return public.replace("//", "/")
|
|
8403
|
+
except Exception:
|
|
8404
|
+
pass
|
|
8405
|
+
for provider_id in sorted(self.providers.keys()):
|
|
8406
|
+
root = self._provider_root_path(provider_id)
|
|
8407
|
+
if root is None:
|
|
8408
|
+
continue
|
|
8409
|
+
try:
|
|
8410
|
+
if root == self.skills_root.resolve() or root.is_relative_to(self.skills_root.resolve()):
|
|
8411
|
+
continue
|
|
8412
|
+
except Exception:
|
|
8413
|
+
pass
|
|
8414
|
+
try:
|
|
8415
|
+
rel = resolved.relative_to(root).as_posix()
|
|
8416
|
+
except Exception:
|
|
8417
|
+
continue
|
|
8418
|
+
prefix = self._provider_virtual_prefix(provider_id)
|
|
8419
|
+
public = prefix if rel in {"", "."} else f"{prefix}/{rel}"
|
|
8420
|
+
return public.replace("//", "/")
|
|
8421
|
+
return ""
|
|
8422
|
+
|
|
8423
|
+
def shell_virtual_mappings(self) -> list[tuple[str, str]]:
|
|
8424
|
+
rows: list[tuple[str, str]] = []
|
|
8425
|
+
try:
|
|
8426
|
+
skills_root = self.skills_root.resolve()
|
|
8427
|
+
except Exception:
|
|
8428
|
+
skills_root = self.skills_root
|
|
8429
|
+
for provider_id in sorted(self.providers.keys()):
|
|
8430
|
+
root = self._provider_root_path(provider_id)
|
|
8431
|
+
if root is None:
|
|
8432
|
+
continue
|
|
8433
|
+
try:
|
|
8434
|
+
if root == skills_root or root.is_relative_to(skills_root):
|
|
8435
|
+
continue
|
|
8436
|
+
except Exception:
|
|
8437
|
+
pass
|
|
8438
|
+
rows.append((self._provider_virtual_prefix(provider_id), str(root)))
|
|
8439
|
+
rows.sort(key=lambda x: len(x[0]), reverse=True)
|
|
8440
|
+
return rows
|
|
8441
|
+
|
|
7156
8442
|
def _public_skill_locator(self, raw_locator: str) -> str:
|
|
7157
8443
|
raw = str(raw_locator or "").strip()
|
|
7158
8444
|
if not raw:
|
|
@@ -7214,12 +8500,15 @@ class SkillStore:
|
|
|
7214
8500
|
def _public_attachment_locator(self, item: dict) -> tuple[str, str]:
|
|
7215
8501
|
source = str(item.get("virtual_path", "") or "").strip()
|
|
7216
8502
|
if not source:
|
|
7217
|
-
source = self.
|
|
8503
|
+
source = self._public_provider_locator(
|
|
8504
|
+
str(item.get("abs_path", "") or ""),
|
|
8505
|
+
str(item.get("provider_id", "") or ""),
|
|
8506
|
+
)
|
|
7218
8507
|
virtual_path = source if source.startswith(f"{SKILLS_VIRTUAL_PREFIX}/") else ""
|
|
7219
8508
|
return source, virtual_path
|
|
7220
8509
|
|
|
7221
|
-
def _public_provider_locator(self, raw_locator: str) -> str:
|
|
7222
|
-
public = self.
|
|
8510
|
+
def _public_provider_locator(self, raw_locator: str, provider_id: str = "") -> str:
|
|
8511
|
+
public = self._public_provider_path(provider_id, raw_locator)
|
|
7223
8512
|
if public == "(external-local-path)":
|
|
7224
8513
|
return ""
|
|
7225
8514
|
return public
|
|
@@ -7255,6 +8544,7 @@ class SkillStore:
|
|
|
7255
8544
|
self,
|
|
7256
8545
|
skill_dir: Path,
|
|
7257
8546
|
*,
|
|
8547
|
+
provider_id: str = "",
|
|
7258
8548
|
skip_name: str = "SKILL.md",
|
|
7259
8549
|
globs: list[str] | None = None,
|
|
7260
8550
|
base_dir: Path | None = None,
|
|
@@ -7299,7 +8589,9 @@ class SkillStore:
|
|
|
7299
8589
|
{
|
|
7300
8590
|
"path": rel,
|
|
7301
8591
|
"abs_path": str(fp.resolve()),
|
|
7302
|
-
"virtual_path":
|
|
8592
|
+
"virtual_path": self._public_provider_path(provider_id, fp.resolve())
|
|
8593
|
+
or f"{SKILLS_VIRTUAL_PREFIX}/{skill_rel}".replace("//", "/"),
|
|
8594
|
+
"provider_id": provider_id,
|
|
7303
8595
|
"content": text or "",
|
|
7304
8596
|
"text_available": text is not None,
|
|
7305
8597
|
"size": size,
|
|
@@ -7338,7 +8630,7 @@ class SkillStore:
|
|
|
7338
8630
|
"protocol_version": protocol_version,
|
|
7339
8631
|
"meta": dict(meta or {}),
|
|
7340
8632
|
"body": (body or "").strip(),
|
|
7341
|
-
"skill_path": self.
|
|
8633
|
+
"skill_path": self._public_provider_path(provider_id, skill_path),
|
|
7342
8634
|
"skill_abs_path": self._absolute_skill_locator(skill_path),
|
|
7343
8635
|
"attachments": attachments,
|
|
7344
8636
|
}
|
|
@@ -7361,6 +8653,7 @@ class SkillStore:
|
|
|
7361
8653
|
desc = str(meta.get("description", "-"))
|
|
7362
8654
|
attachments = self._collect_attachments(
|
|
7363
8655
|
skill_dir,
|
|
8656
|
+
provider_id=provider_id,
|
|
7364
8657
|
skip_name=skill_file.name,
|
|
7365
8658
|
globs=self._skill_attachment_globs(meta),
|
|
7366
8659
|
entrypoints=self._skill_entrypoints(meta),
|
|
@@ -7871,8 +9164,9 @@ class SkillStore:
|
|
|
7871
9164
|
out = []
|
|
7872
9165
|
for provider in sorted(self.providers.values(), key=lambda x: x["provider_id"]):
|
|
7873
9166
|
row = dict(provider)
|
|
7874
|
-
|
|
7875
|
-
row["
|
|
9167
|
+
provider_id = str(row.get("provider_id", "") or "")
|
|
9168
|
+
row["root"] = self._public_provider_locator(str(row.get("root", "") or ""), provider_id)
|
|
9169
|
+
row["config_path"] = self._public_provider_locator(str(row.get("config_path", "") or ""), provider_id)
|
|
7876
9170
|
out.append(row)
|
|
7877
9171
|
return out
|
|
7878
9172
|
|
|
@@ -8006,8 +9300,10 @@ class SkillStore:
|
|
|
8006
9300
|
if not skill_abs_path:
|
|
8007
9301
|
raise ValueError("skill has no local absolute path")
|
|
8008
9302
|
base = Path(skill_abs_path).resolve().parent
|
|
8009
|
-
|
|
8010
|
-
|
|
9303
|
+
virtual_path = self._public_provider_path(
|
|
9304
|
+
str(skill.get("provider_id", "") or ""),
|
|
9305
|
+
(base / rel).resolve(),
|
|
9306
|
+
)
|
|
8011
9307
|
except Exception:
|
|
8012
9308
|
virtual_path = str(row.get("virtual_path", "") or "")
|
|
8013
9309
|
lines.append(
|
|
@@ -9276,6 +10572,116 @@ class OllamaClient:
|
|
|
9276
10572
|
tool_calls = self._normalize_tool_calls(msg.get("tool_calls", []) if isinstance(msg, dict) else [])
|
|
9277
10573
|
return {"content": content, "thinking": thinking_content, "tool_calls": tool_calls, "raw": raw}
|
|
9278
10574
|
|
|
10575
|
+
# ── Anthropic Messages API ─────────────────────────────────────
|
|
10576
|
+
|
|
10577
|
+
def _chat_anthropic(
|
|
10578
|
+
self,
|
|
10579
|
+
req_messages: list[dict],
|
|
10580
|
+
*,
|
|
10581
|
+
tools: list[dict] | None = None,
|
|
10582
|
+
max_tokens: int = 2000,
|
|
10583
|
+
temperature: float = 0.2,
|
|
10584
|
+
think: bool = False,
|
|
10585
|
+
) -> dict:
|
|
10586
|
+
endpoint = (self.endpoint or "").strip()
|
|
10587
|
+
if not endpoint:
|
|
10588
|
+
base = (self.base_url or "").strip().rstrip("/")
|
|
10589
|
+
endpoint = f"{base}/v1/messages" if base else "https://api.anthropic.com/v1/messages"
|
|
10590
|
+
# Separate system messages (Anthropic uses a dedicated 'system' parameter)
|
|
10591
|
+
system_parts: list[str] = []
|
|
10592
|
+
messages: list[dict] = []
|
|
10593
|
+
for m in req_messages:
|
|
10594
|
+
role = str(m.get("role", "") or "").strip()
|
|
10595
|
+
content = m.get("content", "") or ""
|
|
10596
|
+
if role == "system":
|
|
10597
|
+
system_parts.append(str(content))
|
|
10598
|
+
else:
|
|
10599
|
+
# Convert OpenAI-style image_url parts to Anthropic image format
|
|
10600
|
+
if isinstance(content, list):
|
|
10601
|
+
converted: list[dict] = []
|
|
10602
|
+
for part in content:
|
|
10603
|
+
if not isinstance(part, dict):
|
|
10604
|
+
continue
|
|
10605
|
+
if part.get("type") == "image_url":
|
|
10606
|
+
url_data = part.get("image_url", {})
|
|
10607
|
+
url_str = str(url_data.get("url", "") if isinstance(url_data, dict) else url_data or "")
|
|
10608
|
+
dm = re.match(r"^data:([^;]+);base64,(.+)$", url_str, re.DOTALL)
|
|
10609
|
+
if dm:
|
|
10610
|
+
converted.append({
|
|
10611
|
+
"type": "image",
|
|
10612
|
+
"source": {"type": "base64", "media_type": dm.group(1), "data": dm.group(2)},
|
|
10613
|
+
})
|
|
10614
|
+
else:
|
|
10615
|
+
converted.append({"type": "image", "source": {"type": "url", "url": url_str}})
|
|
10616
|
+
else:
|
|
10617
|
+
converted.append(part)
|
|
10618
|
+
content = converted
|
|
10619
|
+
messages.append({"role": role, "content": content})
|
|
10620
|
+
payload: dict = {
|
|
10621
|
+
"model": self.model,
|
|
10622
|
+
"max_tokens": max_tokens,
|
|
10623
|
+
"temperature": temperature,
|
|
10624
|
+
"messages": messages,
|
|
10625
|
+
}
|
|
10626
|
+
if system_parts:
|
|
10627
|
+
payload["system"] = "\n\n".join(system_parts)
|
|
10628
|
+
if tools:
|
|
10629
|
+
payload["tools"] = self._convert_tools_to_anthropic(tools)
|
|
10630
|
+
headers = {
|
|
10631
|
+
"x-api-key": self.api_key,
|
|
10632
|
+
"anthropic-version": "2023-06-01",
|
|
10633
|
+
"content-type": "application/json",
|
|
10634
|
+
}
|
|
10635
|
+
raw = self._post_json_url(endpoint, payload, headers=headers)
|
|
10636
|
+
content, tool_calls, thinking_content = self._extract_anthropic_message(raw)
|
|
10637
|
+
return {"content": content, "thinking": thinking_content, "tool_calls": tool_calls, "raw": raw}
|
|
10638
|
+
|
|
10639
|
+
def _extract_anthropic_message(self, raw: dict) -> tuple[str, list[dict], str]:
|
|
10640
|
+
"""Parse Anthropic Messages API response into (content, tool_calls, thinking)."""
|
|
10641
|
+
content_blocks = raw.get("content", [])
|
|
10642
|
+
if not isinstance(content_blocks, list):
|
|
10643
|
+
# Fallback: try OpenAI format
|
|
10644
|
+
if isinstance(raw.get("choices"), list):
|
|
10645
|
+
return self._extract_openai_message(raw)
|
|
10646
|
+
return str(content_blocks or ""), [], ""
|
|
10647
|
+
text_parts: list[str] = []
|
|
10648
|
+
thinking_parts: list[str] = []
|
|
10649
|
+
tool_calls: list[dict] = []
|
|
10650
|
+
for block in content_blocks:
|
|
10651
|
+
if not isinstance(block, dict):
|
|
10652
|
+
continue
|
|
10653
|
+
btype = str(block.get("type", "") or "").strip()
|
|
10654
|
+
if btype == "text":
|
|
10655
|
+
text_parts.append(str(block.get("text", "") or ""))
|
|
10656
|
+
elif btype == "thinking":
|
|
10657
|
+
thinking_parts.append(str(block.get("thinking", "") or ""))
|
|
10658
|
+
elif btype == "tool_use":
|
|
10659
|
+
tool_calls.append({
|
|
10660
|
+
"id": str(block.get("id") or make_id("tool")),
|
|
10661
|
+
"type": "function",
|
|
10662
|
+
"function": {
|
|
10663
|
+
"name": str(block.get("name", "") or ""),
|
|
10664
|
+
"arguments": json_dumps(block.get("input", {})),
|
|
10665
|
+
},
|
|
10666
|
+
})
|
|
10667
|
+
return "\n".join(text_parts), tool_calls, "\n".join(thinking_parts)
|
|
10668
|
+
|
|
10669
|
+
def _convert_tools_to_anthropic(self, openai_tools: list[dict]) -> list[dict]:
|
|
10670
|
+
"""Convert OpenAI-format tool definitions to Anthropic format."""
|
|
10671
|
+
out: list[dict] = []
|
|
10672
|
+
for t in openai_tools:
|
|
10673
|
+
if not isinstance(t, dict):
|
|
10674
|
+
continue
|
|
10675
|
+
fn = t.get("function", {})
|
|
10676
|
+
if not isinstance(fn, dict):
|
|
10677
|
+
continue
|
|
10678
|
+
out.append({
|
|
10679
|
+
"name": str(fn.get("name", "") or ""),
|
|
10680
|
+
"description": str(fn.get("description", "") or ""),
|
|
10681
|
+
"input_schema": fn.get("parameters", {"type": "object", "properties": {}}),
|
|
10682
|
+
})
|
|
10683
|
+
return out
|
|
10684
|
+
|
|
9279
10685
|
def _chat_ollama_stream_native(
|
|
9280
10686
|
self,
|
|
9281
10687
|
req_messages: list[dict],
|
|
@@ -9401,6 +10807,10 @@ class OllamaClient:
|
|
|
9401
10807
|
return self._chat_openai_compat(
|
|
9402
10808
|
req_messages, tools=tools, max_tokens=max_tokens, temperature=temperature, think=False
|
|
9403
10809
|
)
|
|
10810
|
+
if provider == "anthropic":
|
|
10811
|
+
return self._chat_anthropic(
|
|
10812
|
+
req_messages, tools=tools, max_tokens=max_tokens, temperature=temperature, think=False
|
|
10813
|
+
)
|
|
9404
10814
|
if provider == "custom_http":
|
|
9405
10815
|
return self._chat_custom_http(
|
|
9406
10816
|
req_messages, tools=tools, max_tokens=max_tokens, temperature=temperature, think=False
|
|
@@ -10484,6 +11894,11 @@ class SessionState:
|
|
|
10484
11894
|
if provider == "custom_http":
|
|
10485
11895
|
endpoint = str(profile.get("endpoint", "") or "").strip()
|
|
10486
11896
|
return bool(endpoint)
|
|
11897
|
+
if provider == "anthropic":
|
|
11898
|
+
api_key = str(profile.get("api_key", "") or "").strip()
|
|
11899
|
+
endpoint = str(profile.get("endpoint", "") or "").strip()
|
|
11900
|
+
base = str(profile.get("base_url", "") or "").strip()
|
|
11901
|
+
return bool(api_key and (endpoint or base))
|
|
10487
11902
|
return False
|
|
10488
11903
|
|
|
10489
11904
|
def _option_is_runnable(self, option: dict) -> bool:
|
|
@@ -14205,7 +15620,7 @@ class SessionState:
|
|
|
14205
15620
|
normalized = self._normalize_tool_path_text(path_text)
|
|
14206
15621
|
if normalized == ".__skills__" or normalized.startswith(".__skills__/"):
|
|
14207
15622
|
rel = normalized[len(".__skills__") :].lstrip("/")
|
|
14208
|
-
return
|
|
15623
|
+
return self.skills.resolve_virtual_skill_path(rel or ".")
|
|
14209
15624
|
if normalized == ".__js_lib__" or normalized.startswith(".__js_lib__/"):
|
|
14210
15625
|
rel = normalized[len(".__js_lib__") :].lstrip("/")
|
|
14211
15626
|
return safe_path(rel or ".", self.js_lib_root)
|
|
@@ -14224,10 +15639,9 @@ class SessionState:
|
|
|
14224
15639
|
except Exception:
|
|
14225
15640
|
pass
|
|
14226
15641
|
try:
|
|
14227
|
-
|
|
14228
|
-
if
|
|
14229
|
-
|
|
14230
|
-
return f"{SKILLS_VIRTUAL_PREFIX}/{rel}".replace("//", "/")
|
|
15642
|
+
virtual_skill_path = self.skills.public_virtual_skill_path_for_abs(target)
|
|
15643
|
+
if virtual_skill_path:
|
|
15644
|
+
return virtual_skill_path
|
|
14231
15645
|
except Exception:
|
|
14232
15646
|
pass
|
|
14233
15647
|
return str(target)
|
|
@@ -16618,7 +18032,7 @@ class SessionState:
|
|
|
16618
18032
|
js_lib_root = str(self.js_lib_root.resolve())
|
|
16619
18033
|
except Exception:
|
|
16620
18034
|
js_lib_root = str(self.js_lib_root)
|
|
16621
|
-
mappings = [
|
|
18035
|
+
mappings = self.skills.shell_virtual_mappings() + [
|
|
16622
18036
|
("/workspace", workspace_root),
|
|
16623
18037
|
(SKILLS_VIRTUAL_PREFIX, skills_root),
|
|
16624
18038
|
("/js_lib", js_lib_root),
|
|
@@ -19132,6 +20546,7 @@ class SessionState:
|
|
|
19132
20546
|
clean_todos.append({
|
|
19133
20547
|
"id": trim(str(pt.get("id", "") or ""), 20),
|
|
19134
20548
|
"content": trim(str(pt.get("content", "") or ""), 400),
|
|
20549
|
+
"full_content": trim(str(pt.get("full_content", "") or ""), 1500),
|
|
19135
20550
|
"status": str(pt.get("status", "pending") or "pending") if str(pt.get("status", "pending") or "pending") in ("pending", "in_progress", "completed") else "pending",
|
|
19136
20551
|
"category": trim(str(pt.get("category", "") or ""), 40),
|
|
19137
20552
|
"plan_step_index": int(pt.get("plan_step_index", -1)) if pt.get("plan_step_index") is not None else -1,
|
|
@@ -20226,9 +21641,30 @@ class SessionState:
|
|
|
20226
21641
|
pass
|
|
20227
21642
|
# Immediately sync todos so UI reflects plan step advancement
|
|
20228
21643
|
self._sync_todos_from_blackboard(reason=f"plan-step-advanced:{cursor + 1}", board=bb)
|
|
21644
|
+
# Inject hint for the next step (works in both single and multi-agent mode)
|
|
20229
21645
|
if next_step:
|
|
20230
21646
|
try:
|
|
20231
|
-
|
|
21647
|
+
_ns_idx = int(next_step.get("plan_step_index", 0) or 0) + 1
|
|
21648
|
+
_ns_total = int(bb.get("plan_step_total", 0) or 0)
|
|
21649
|
+
_ns_text = trim(str(next_step.get("content", "") or ""), 200)
|
|
21650
|
+
_ns_id = str(next_step.get("id", "") or "")
|
|
21651
|
+
_ns_label = f"Step {_ns_idx}" + (f"/{_ns_total}" if _ns_total else "")
|
|
21652
|
+
_hint = (
|
|
21653
|
+
f"[plan-step-advance] Previous step completed. Now at {_ns_label}: {_ns_text}\n"
|
|
21654
|
+
f"Read updated plan: read_file {PLAN_FILE_RELATIVE_PATH}\n"
|
|
21655
|
+
f"Call TodoWrite to set subtasks for THIS step ONLY.\n"
|
|
21656
|
+
f"Each subtask MUST include parent_step_id='{_ns_id}'. "
|
|
21657
|
+
f"Create 3-5 items, one marked in_progress, others pending.\n"
|
|
21658
|
+
f"Do NOT create subtasks for other plan steps."
|
|
21659
|
+
)
|
|
21660
|
+
self.messages.append({"role": "system", "content": _hint, "ts": now_ts()})
|
|
21661
|
+
# Also inject into active agent context for multi-agent mode
|
|
21662
|
+
if self._is_multi_agent_mode():
|
|
21663
|
+
active_role = str(bb.get("active_agent", "") or actor)
|
|
21664
|
+
if active_role:
|
|
21665
|
+
self._append_agent_context_message(active_role, {
|
|
21666
|
+
"role": "system", "content": _hint, "ts": now_ts(), "agent_role": active_role,
|
|
21667
|
+
}, mirror_to_global=False)
|
|
20232
21668
|
except Exception:
|
|
20233
21669
|
pass
|
|
20234
21670
|
return True
|
|
@@ -20249,11 +21685,30 @@ class SessionState:
|
|
|
20249
21685
|
worker_produced_output = self._worker_step_has_evidence(worker_step)
|
|
20250
21686
|
# 3. All subtasks for this step are completed
|
|
20251
21687
|
subtasks_all_done = self._step_subtasks_all_completed(current)
|
|
20252
|
-
#
|
|
21688
|
+
# 4. File-evidence fallback: when worker doesn't call TodoWrite, use phase heuristics
|
|
21689
|
+
step_content = str(current.get("full_content", "") or current.get("content", "") or "").lower()
|
|
21690
|
+
phase = self._plan_step_phase_hint(step_content)
|
|
21691
|
+
results = worker_step.get("tool_results", []) or []
|
|
21692
|
+
wrote_files_count = sum(
|
|
21693
|
+
1 for r in results
|
|
21694
|
+
if isinstance(r, dict) and r.get("ok", False)
|
|
21695
|
+
and str(r.get("name", "")) in ("write_file", "edit_file")
|
|
21696
|
+
)
|
|
21697
|
+
ran_bash_ok = any(
|
|
21698
|
+
isinstance(r, dict) and r.get("ok", False) and str(r.get("name", "")) == "bash"
|
|
21699
|
+
for r in results
|
|
21700
|
+
)
|
|
21701
|
+
file_evidence_strong = (
|
|
21702
|
+
phase in ("implement", "design") and wrote_files_count >= 2
|
|
21703
|
+
) or (
|
|
21704
|
+
phase in ("test", "review") and ran_bash_ok
|
|
21705
|
+
)
|
|
21706
|
+
# Advance when:
|
|
20253
21707
|
# - Manager requested AND worker produced output, OR
|
|
20254
|
-
# - All subtasks completed AND worker produced output
|
|
21708
|
+
# - All subtasks completed AND worker produced output, OR
|
|
21709
|
+
# - Strong file evidence (fallback when worker forgets TodoWrite)
|
|
20255
21710
|
has_strong_evidence = worker_produced_output and (
|
|
20256
|
-
manager_requested or subtasks_all_done
|
|
21711
|
+
manager_requested or subtasks_all_done or file_evidence_strong
|
|
20257
21712
|
)
|
|
20258
21713
|
if has_strong_evidence:
|
|
20259
21714
|
evidence = self._collect_step_evidence(current, worker_step)
|
|
@@ -20275,7 +21730,8 @@ class SessionState:
|
|
|
20275
21730
|
)
|
|
20276
21731
|
|
|
20277
21732
|
def _step_subtasks_all_completed(self, plan_step: dict) -> bool:
|
|
20278
|
-
"""Check if all worker subtasks linked to this plan step are completed.
|
|
21733
|
+
"""Check if all worker subtasks linked to this plan step are completed.
|
|
21734
|
+
Filters out cross-step subtasks (e.g., 2.1 under step 1) to prevent blocking."""
|
|
20279
21735
|
step_id = str(plan_step.get("id", "") or "")
|
|
20280
21736
|
if not step_id:
|
|
20281
21737
|
return False
|
|
@@ -20288,6 +21744,23 @@ class SessionState:
|
|
|
20288
21744
|
]
|
|
20289
21745
|
if not worker_items:
|
|
20290
21746
|
return False
|
|
21747
|
+
# Extract major step number from plan step content (e.g., "1. Project init" → "1")
|
|
21748
|
+
import re
|
|
21749
|
+
step_content = str(plan_step.get("full_content", "") or plan_step.get("content", "") or "")
|
|
21750
|
+
_m = re.match(r"^(\d+)\.", step_content.strip())
|
|
21751
|
+
active_major = _m.group(1) if _m else ""
|
|
21752
|
+
# Filter out cross-step subtasks (N.M where N != active major)
|
|
21753
|
+
if active_major:
|
|
21754
|
+
_cross_re = re.compile(r"^(\d+)\.\d+\s")
|
|
21755
|
+
relevant = []
|
|
21756
|
+
for r in worker_items:
|
|
21757
|
+
rc = str(r.get("content", "") or "").strip()
|
|
21758
|
+
cm = _cross_re.match(rc)
|
|
21759
|
+
if cm and cm.group(1) != active_major:
|
|
21760
|
+
continue # Skip cross-step items — don't let them block advancement
|
|
21761
|
+
relevant.append(r)
|
|
21762
|
+
if relevant:
|
|
21763
|
+
worker_items = relevant
|
|
20291
21764
|
return all(str(r.get("status", "")).lower() == "completed" for r in worker_items)
|
|
20292
21765
|
|
|
20293
21766
|
def _collect_step_evidence(self, plan_step: dict, worker_step: dict) -> str:
|
|
@@ -20327,9 +21800,7 @@ class SessionState:
|
|
|
20327
21800
|
self._sync_todos_from_blackboard(reason="single-agent-round")
|
|
20328
21801
|
return
|
|
20329
21802
|
# Heuristic: check if tool results indicate step completion
|
|
20330
|
-
|
|
20331
|
-
# - successful bash calls suggest testing/verification
|
|
20332
|
-
step_content = str(current.get("content", "") or "").lower()
|
|
21803
|
+
step_content = str(current.get("full_content", "") or current.get("content", "") or "").lower()
|
|
20333
21804
|
phase = self._plan_step_phase_hint(step_content)
|
|
20334
21805
|
wrote_files = any(
|
|
20335
21806
|
str(r.get("name", "")) in ("write_file", "edit_file") and r.get("ok", False)
|
|
@@ -20339,19 +21810,23 @@ class SessionState:
|
|
|
20339
21810
|
str(r.get("name", "")) == "bash" and r.get("ok", False)
|
|
20340
21811
|
for r in tool_results
|
|
20341
21812
|
)
|
|
20342
|
-
# Auto-advance conditions
|
|
21813
|
+
# Auto-advance conditions:
|
|
20343
21814
|
should_advance = False
|
|
20344
|
-
if
|
|
20345
|
-
|
|
20346
|
-
|
|
20347
|
-
elif phase == "implement" and wrote_files and ran_bash_ok:
|
|
20348
|
-
# Implementation completed when files written and bash succeeds
|
|
20349
|
-
should_advance = True
|
|
20350
|
-
elif phase in ("test", "review") and ran_bash_ok and not any(
|
|
20351
|
-
not r.get("ok", False) for r in tool_results if str(r.get("name", "")) == "bash"
|
|
20352
|
-
):
|
|
20353
|
-
# Test/review completed when all bash calls succeed
|
|
21815
|
+
# Priority 1: Check if worker subtasks are all completed (most reliable signal)
|
|
21816
|
+
subtasks_done = self._step_subtasks_all_completed(current)
|
|
21817
|
+
if subtasks_done and (wrote_files or ran_bash_ok):
|
|
20354
21818
|
should_advance = True
|
|
21819
|
+
# Priority 2: Phase-based heuristics (relaxed — wrote_files OR bash, not both)
|
|
21820
|
+
if not should_advance:
|
|
21821
|
+
if phase in ("research", "design") and wrote_files:
|
|
21822
|
+
should_advance = True
|
|
21823
|
+
elif phase == "implement" and wrote_files:
|
|
21824
|
+
# Relaxed: implement step done when files are written (don't require bash)
|
|
21825
|
+
should_advance = True
|
|
21826
|
+
elif phase in ("test", "review") and ran_bash_ok and not any(
|
|
21827
|
+
not r.get("ok", False) for r in tool_results if str(r.get("name", "")) == "bash"
|
|
21828
|
+
):
|
|
21829
|
+
should_advance = True
|
|
20355
21830
|
# Also check if the agent explicitly mentioned step completion
|
|
20356
21831
|
if not should_advance:
|
|
20357
21832
|
# Check last assistant message for step completion signals
|
|
@@ -20379,12 +21854,15 @@ class SessionState:
|
|
|
20379
21854
|
_step_idx = int(_new_step.get("plan_step_index", 0) or 0) + 1
|
|
20380
21855
|
_total = int(_bb_after.get("plan_step_total", 0) or 0)
|
|
20381
21856
|
_step_text = trim(str(_new_step.get("content", "") or ""), 200)
|
|
21857
|
+
_step_id = str(_new_step.get("id", "") or "")
|
|
20382
21858
|
_step_label = f"Step {_step_idx}" + (f"/{_total}" if _total else "")
|
|
20383
21859
|
_hint = (
|
|
20384
21860
|
f"[plan-step-advance] Previous step completed. Now at {_step_label}: {_step_text}\n"
|
|
20385
21861
|
f"Read updated plan: read_file {PLAN_FILE_RELATIVE_PATH}\n"
|
|
20386
|
-
"Call TodoWrite to set
|
|
20387
|
-
"
|
|
21862
|
+
f"Call TodoWrite to set subtasks for THIS step ONLY.\n"
|
|
21863
|
+
f"Each subtask MUST include parent_step_id='{_step_id}'. "
|
|
21864
|
+
f"Create 3-5 items, one marked in_progress, others pending.\n"
|
|
21865
|
+
f"Do NOT create subtasks for other plan steps."
|
|
20388
21866
|
)
|
|
20389
21867
|
self.messages.append({"role": "system", "content": _hint, "ts": now_ts()})
|
|
20390
21868
|
except Exception:
|
|
@@ -20445,6 +21923,58 @@ class SessionState:
|
|
|
20445
21923
|
active_system = [r for r in system_rows if r.get("status") != "completed"]
|
|
20446
21924
|
completed_system = [r for r in system_rows if r.get("status") == "completed"]
|
|
20447
21925
|
trimmed_system = active_system + completed_system[-3:]
|
|
21926
|
+
# ── Subtask conflict guard ──
|
|
21927
|
+
# Remove worker subtasks whose content duplicates a plan step to prevent
|
|
21928
|
+
# the UI from showing the same task twice (once as plan step, once as subtask).
|
|
21929
|
+
if trimmed_system:
|
|
21930
|
+
import re as _re_dedup
|
|
21931
|
+
plan_content_set = set()
|
|
21932
|
+
for sr in trimmed_system:
|
|
21933
|
+
pc = str(sr.get("content", "") or "").strip().lower()
|
|
21934
|
+
if pc:
|
|
21935
|
+
plan_content_set.add(pc)
|
|
21936
|
+
# Also add first line only (sub-step headers match full step content)
|
|
21937
|
+
first_line = pc.split("\n")[0].strip()
|
|
21938
|
+
if first_line:
|
|
21939
|
+
plan_content_set.add(first_line)
|
|
21940
|
+
# Build stripped-prefix set for fuzzy matching ("步骤 1:XXX" → "XXX")
|
|
21941
|
+
_num_prefix_re = _re_dedup.compile(r"^(?:步骤\s*\d+[::]\s*|\d+\.\s*|step\s*\d+[::]\s*)", _re_dedup.IGNORECASE)
|
|
21942
|
+
plan_stripped_set = set()
|
|
21943
|
+
for sr in trimmed_system:
|
|
21944
|
+
pc = str(sr.get("content", "") or "").strip().lower()
|
|
21945
|
+
stripped = _num_prefix_re.sub("", pc).strip()
|
|
21946
|
+
if stripped and len(stripped) > 4:
|
|
21947
|
+
plan_stripped_set.add(stripped)
|
|
21948
|
+
# Find current active step major number for cross-step detection
|
|
21949
|
+
_active_major = ""
|
|
21950
|
+
for sr in trimmed_system:
|
|
21951
|
+
if sr.get("status") == "in_progress":
|
|
21952
|
+
mc = str(sr.get("content", "") or "")
|
|
21953
|
+
_am = _re_dedup.match(r"^(\d+)\.", mc)
|
|
21954
|
+
if _am:
|
|
21955
|
+
_active_major = _am.group(1)
|
|
21956
|
+
break
|
|
21957
|
+
_cross_step_re = _re_dedup.compile(r"^(\d+)\.\d+\s")
|
|
21958
|
+
deduped_worker = []
|
|
21959
|
+
for wr in worker_rows:
|
|
21960
|
+
wc = str(wr.get("content", "") or "").strip().lower()
|
|
21961
|
+
if not wc:
|
|
21962
|
+
deduped_worker.append(wr)
|
|
21963
|
+
continue
|
|
21964
|
+
# Layer 1: Exact match
|
|
21965
|
+
if wc in plan_content_set:
|
|
21966
|
+
continue
|
|
21967
|
+
# Layer 2: Stripped prefix match
|
|
21968
|
+
wc_stripped = _num_prefix_re.sub("", wc).strip()
|
|
21969
|
+
if wc_stripped and wc_stripped in plan_stripped_set:
|
|
21970
|
+
continue
|
|
21971
|
+
# Layer 3: Cross-step detection — reject N.M subtasks where N != active major
|
|
21972
|
+
if _active_major:
|
|
21973
|
+
cm = _cross_step_re.match(wc)
|
|
21974
|
+
if cm and cm.group(1) != _active_major:
|
|
21975
|
+
continue
|
|
21976
|
+
deduped_worker.append(wr)
|
|
21977
|
+
worker_rows = deduped_worker
|
|
20448
21978
|
remaining_cap = max(0, 40 - len(trimmed_system) - len(worker_rows))
|
|
20449
21979
|
merged = list(trimmed_system) + worker_rows + non_system_rows[:remaining_cap]
|
|
20450
21980
|
try:
|
|
@@ -21240,6 +22770,13 @@ class SessionState:
|
|
|
21240
22770
|
_prev_level_val = int(getattr(self, '_prev_applied_task_level', 0) or 0)
|
|
21241
22771
|
if int(getattr(self, 'user_task_level_override', 0) or 0) > 0:
|
|
21242
22772
|
level = int(self.user_task_level_override)
|
|
22773
|
+
# Floor protection: if plan was approved, do not allow downgrade below floor
|
|
22774
|
+
_level_floor = int(getattr(self, 'runtime_task_level_floor', 0) or 0)
|
|
22775
|
+
if _level_floor > 0 and int(level) < _level_floor:
|
|
22776
|
+
level = _level_floor
|
|
22777
|
+
_complexity_floor = str(getattr(self, 'runtime_complexity_floor', '') or '').strip()
|
|
22778
|
+
if _complexity_floor == "complex" and complexity == "simple":
|
|
22779
|
+
complexity = "complex"
|
|
21243
22780
|
self.runtime_task_level = int(level)
|
|
21244
22781
|
self._prev_applied_task_level = int(level)
|
|
21245
22782
|
self.runtime_execution_mode = mode
|
|
@@ -21609,7 +23146,7 @@ class SessionState:
|
|
|
21609
23146
|
idx = int(t.get("plan_step_index", 0) or 0) + 1
|
|
21610
23147
|
status = t.get("status", "pending")
|
|
21611
23148
|
mark = "✅" if status == "completed" else "👉" if status == "in_progress" else "⬜"
|
|
21612
|
-
phase_hint = self._plan_step_phase_hint(str(t.get("content", "") or ""))
|
|
23149
|
+
phase_hint = self._plan_step_phase_hint(str(t.get("full_content", "") or t.get("content", "") or ""))
|
|
21613
23150
|
phase_tag = f" [{phase_hint}]" if phase_hint else ""
|
|
21614
23151
|
lines.append(f" {mark} Step {idx}: {trim(str(t.get('content', '') or ''), 160)}{phase_tag}")
|
|
21615
23152
|
lines.append("Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing. ")
|
|
@@ -21672,7 +23209,7 @@ class SessionState:
|
|
|
21672
23209
|
bb = self._ensure_blackboard()
|
|
21673
23210
|
for t in bb.get("project_todos", []):
|
|
21674
23211
|
if t.get("category") == "plan_step" and t.get("status") == "in_progress":
|
|
21675
|
-
phase = self._plan_step_phase_hint(str(t.get("content", "") or ""))
|
|
23212
|
+
phase = self._plan_step_phase_hint(str(t.get("full_content", "") or t.get("content", "") or ""))
|
|
21676
23213
|
if phase:
|
|
21677
23214
|
return phase
|
|
21678
23215
|
# Fallback: infer from blackboard state
|
|
@@ -23447,7 +24984,7 @@ class SessionState:
|
|
|
23447
24984
|
if isinstance(plan_todos, list):
|
|
23448
24985
|
for pt in plan_todos:
|
|
23449
24986
|
if isinstance(pt, dict) and pt.get("category") == "plan_step" and pt.get("status") == "in_progress":
|
|
23450
|
-
step_text = trim(str(pt.get("content", "") or ""),
|
|
24987
|
+
step_text = trim(str(pt.get("full_content", "") or pt.get("content", "") or ""), 600)
|
|
23451
24988
|
step_idx = int(pt.get("plan_step_index", 0) or 0) + 1
|
|
23452
24989
|
current_plan_step_note = (
|
|
23453
24990
|
f"CURRENT PLAN STEP (#{step_idx}): {step_text}\n"
|
|
@@ -23465,10 +25002,14 @@ class SessionState:
|
|
|
23465
25002
|
_active_step_id = str(_pt.get("id", "") or "")
|
|
23466
25003
|
break
|
|
23467
25004
|
todo_update_note = (
|
|
23468
|
-
f"TODO UPDATE:
|
|
23469
|
-
f"Each subtask MUST include parent_step_id='{_active_step_id}'
|
|
23470
|
-
f"
|
|
23471
|
-
f"
|
|
25005
|
+
f"TODO UPDATE: Call TodoWrite at the START to set subtasks for THIS step ONLY.\n"
|
|
25006
|
+
f"Each subtask MUST include parent_step_id='{_active_step_id}'.\n"
|
|
25007
|
+
f"CRITICAL SCOPE RULE:\n"
|
|
25008
|
+
f"- Create 3-5 subtasks that break down ONLY the current step's work.\n"
|
|
25009
|
+
f"- Do NOT create subtasks for other plan steps (do NOT list step 2, 3, 4 etc.).\n"
|
|
25010
|
+
f"- Do NOT duplicate the plan step titles as subtasks.\n"
|
|
25011
|
+
f"- Each subtask should be a concrete action within THIS step.\n"
|
|
25012
|
+
f"Mark each subtask completed as you finish it. When ALL are done, the step auto-advances.\n"
|
|
23472
25013
|
)
|
|
23473
25014
|
# Build step_files context note for cross-agent file visibility
|
|
23474
25015
|
step_files_note = ""
|
|
@@ -26907,8 +28448,15 @@ class SessionState:
|
|
|
26907
28448
|
self.runtime_plan_approved = True
|
|
26908
28449
|
return
|
|
26909
28450
|
|
|
26910
|
-
#
|
|
28451
|
+
# Synthesis Step 1: 立即写 plan.md(与 synthesis 思维连续,避免信息丢失)
|
|
26911
28452
|
self.runtime_plan_proposal = proposal
|
|
28453
|
+
try:
|
|
28454
|
+
self._write_plan_file(self._format_plan_file_preselection(proposal))
|
|
28455
|
+
except Exception:
|
|
28456
|
+
pass
|
|
28457
|
+
self._emit("status", {"summary": "plan-mode: plan.md written"})
|
|
28458
|
+
|
|
28459
|
+
# Synthesis Step 2: 更新 blackboard + 生成精简 bubble
|
|
26912
28460
|
bb = self._ensure_blackboard()
|
|
26913
28461
|
if not isinstance(bb.get("plan"), dict):
|
|
26914
28462
|
bb["plan"] = {"phase": "awaiting_choice", "findings": []}
|
|
@@ -26916,13 +28464,7 @@ class SessionState:
|
|
|
26916
28464
|
bb["plan"]["proposal"] = proposal
|
|
26917
28465
|
self.blackboard = bb
|
|
26918
28466
|
|
|
26919
|
-
#
|
|
26920
|
-
try:
|
|
26921
|
-
self._write_plan_file(self._format_plan_file_preselection(proposal))
|
|
26922
|
-
except Exception:
|
|
26923
|
-
pass
|
|
26924
|
-
|
|
26925
|
-
# Condensed bubble for UI (under PLAN_BUBBLE_MAX_CHARS)
|
|
28467
|
+
# Phase 3: Emit bubble 到前端(纯输出,不做额外思考)
|
|
26926
28468
|
bubble_text = self._format_plan_bubble_preselection(proposal)
|
|
26927
28469
|
self.messages.append({
|
|
26928
28470
|
"role": "assistant",
|
|
@@ -27445,7 +28987,38 @@ class SessionState:
|
|
|
27445
28987
|
f"- When a loaded skill defines a specific workflow, follow that workflow's actual tools and scripts.\n"
|
|
27446
28988
|
f"- For complex tasks, produce 8-15 detailed steps, not 3-5 vague ones\n"
|
|
27447
28989
|
f"- Each step should be completable in 1-3 tool calls\n"
|
|
27448
|
-
f"
|
|
28990
|
+
f"\nSTEP STRUCTURE — MAJOR STEPS WITH SUB-STEPS:\n"
|
|
28991
|
+
f"Organize steps into MAJOR numbered groups. Each major step has:\n"
|
|
28992
|
+
f" 1) A summary title line: \"N. Summary Title\" (e.g., \"1. Project Initialization\")\n"
|
|
28993
|
+
f" 2) Sub-steps: \"N.1 Sub-step title\\nConcrete details\\nN.2 Next sub-step\\nDetails\"\n"
|
|
28994
|
+
f"\nThe steps array should contain one string per MAJOR step. "
|
|
28995
|
+
f"Each string starts with the summary title, followed by sub-steps.\n"
|
|
28996
|
+
f"\nExample steps array with 3 major steps:\n"
|
|
28997
|
+
f"[\n"
|
|
28998
|
+
f" \"1. Project Initialization and Build\\n"
|
|
28999
|
+
f"1.1 Initialize project structure\\n"
|
|
29000
|
+
f"Create directories: src/python/, src/fortran/, tests/, docs/\\n"
|
|
29001
|
+
f"1.2 Configure build system\\n"
|
|
29002
|
+
f"Create CMakeLists.txt with Fortran compiler config\\n"
|
|
29003
|
+
f"Run: cmake -B build -S . && cmake --build build\",\n"
|
|
29004
|
+
f" \"2. Core Module Implementation\\n"
|
|
29005
|
+
f"2.1 Implement data model\\n"
|
|
29006
|
+
f"Create src/models/data.py with schema definitions\\n"
|
|
29007
|
+
f"2.2 Implement business logic\\n"
|
|
29008
|
+
f"Create src/services/processor.py\",\n"
|
|
29009
|
+
f" \"3. Testing and Documentation\\n"
|
|
29010
|
+
f"3.1 Unit tests\\n"
|
|
29011
|
+
f"Create tests/test_models.py\\n"
|
|
29012
|
+
f"Run: pytest tests/\\n"
|
|
29013
|
+
f"3.2 Write documentation\\n"
|
|
29014
|
+
f"Create docs/README.md\"\n"
|
|
29015
|
+
f"]\n"
|
|
29016
|
+
f"\nRules:\n"
|
|
29017
|
+
f"- Each major step = one array element with summary title + 2-5 sub-steps\n"
|
|
29018
|
+
f"- Summary title captures the THEME of that group (not just repeat the first sub-step)\n"
|
|
29019
|
+
f"- Sub-steps include specific file paths, commands, or expected outputs\n"
|
|
29020
|
+
f"- Aim for 5-8 major steps for complex tasks, 3-5 for simpler ones\n"
|
|
29021
|
+
f"\n"
|
|
27449
29022
|
f"Make options meaningfully different (e.g. different approaches, scope levels, or trade-offs).\n"
|
|
27450
29023
|
"\nVERIFICATION & TESTING:\n"
|
|
27451
29024
|
"Judge from the task content and research findings whether the task involves writing, "
|
|
@@ -27627,8 +29200,22 @@ class SessionState:
|
|
|
27627
29200
|
steps = opt.get("steps", [])
|
|
27628
29201
|
if isinstance(steps, list) and steps:
|
|
27629
29202
|
lines.append("\n### Steps")
|
|
29203
|
+
import re as _re_plan
|
|
29204
|
+
_mid_re = _re_plan.compile(r"(?<=\S)\s+(\d+\.\d+\s)")
|
|
27630
29205
|
for i, s in enumerate(steps):
|
|
27631
|
-
|
|
29206
|
+
step_str = str(s or "").strip()
|
|
29207
|
+
# Normalize: split mid-string N.N sub-step numbers onto own lines
|
|
29208
|
+
step_str = _mid_re.sub(r"\n\1", step_str)
|
|
29209
|
+
if "\n" in step_str:
|
|
29210
|
+
# Multi-line step: first line as header, rest as nested list
|
|
29211
|
+
step_lines = step_str.split("\n")
|
|
29212
|
+
lines.append(f"{i + 1}. {step_lines[0]}")
|
|
29213
|
+
for sub in step_lines[1:]:
|
|
29214
|
+
stripped = sub.strip()
|
|
29215
|
+
if stripped:
|
|
29216
|
+
lines.append(f" - {stripped}")
|
|
29217
|
+
else:
|
|
29218
|
+
lines.append(f"{i + 1}. {step_str}")
|
|
27632
29219
|
pros = str(opt.get("pros", "") or "").strip()
|
|
27633
29220
|
if pros:
|
|
27634
29221
|
lines.append(f"\n**Pros:** {pros}")
|
|
@@ -27668,14 +29255,22 @@ class SessionState:
|
|
|
27668
29255
|
if summary:
|
|
27669
29256
|
lines.append(f"## Summary\n{summary}\n")
|
|
27670
29257
|
lines.append("## Steps\n")
|
|
29258
|
+
import re as _re_exec
|
|
29259
|
+
_mid_re_exec = _re_exec.compile(r"(?<=\S)\s+(\d+\.\d+\s)")
|
|
27671
29260
|
for t in plan_todos:
|
|
27672
29261
|
idx = int(t.get("plan_step_index", 0) or 0) + 1
|
|
27673
|
-
|
|
29262
|
+
full = str(t.get("full_content", "") or t.get("content", "")).strip()
|
|
29263
|
+
# Normalize: split concatenated N.N sub-steps onto own lines
|
|
29264
|
+
full = _mid_re_exec.sub(r"\n\1", full)
|
|
29265
|
+
header = full.split("\n")[0] if "\n" in full else full
|
|
29266
|
+
sub_lines = [ln for ln in full.split("\n")[1:] if ln.strip()] if "\n" in full else []
|
|
27674
29267
|
status = str(t.get("status", "pending") or "pending")
|
|
27675
29268
|
if status == "completed":
|
|
27676
29269
|
actor = str(t.get("completed_by", "") or "")
|
|
27677
29270
|
evidence = str(t.get("evidence", "") or "")
|
|
27678
|
-
lines.append(f"- [x] Step {idx}: {
|
|
29271
|
+
lines.append(f"- [x] Step {idx}: {header}")
|
|
29272
|
+
for sub in sub_lines:
|
|
29273
|
+
lines.append(f" - {sub.strip()}")
|
|
27679
29274
|
meta_parts = []
|
|
27680
29275
|
if actor:
|
|
27681
29276
|
meta_parts.append(f"Completed by: {actor}")
|
|
@@ -27684,9 +29279,13 @@ class SessionState:
|
|
|
27684
29279
|
if meta_parts:
|
|
27685
29280
|
lines.append(f" > {' | '.join(meta_parts)}")
|
|
27686
29281
|
elif status == "in_progress":
|
|
27687
|
-
lines.append(f"- [>] Step {idx}: {
|
|
29282
|
+
lines.append(f"- [>] Step {idx}: {header} <-- CURRENT")
|
|
29283
|
+
for sub in sub_lines:
|
|
29284
|
+
lines.append(f" - {sub.strip()}")
|
|
27688
29285
|
else:
|
|
27689
|
-
lines.append(f"- [ ] Step {idx}: {
|
|
29286
|
+
lines.append(f"- [ ] Step {idx}: {header}")
|
|
29287
|
+
for sub in sub_lines:
|
|
29288
|
+
lines.append(f" - {sub.strip()}")
|
|
27690
29289
|
return "\n".join(lines) + "\n"
|
|
27691
29290
|
|
|
27692
29291
|
def _update_plan_file_step_status(self) -> bool:
|
|
@@ -27739,14 +29338,121 @@ class SessionState:
|
|
|
27739
29338
|
|
|
27740
29339
|
def _plan_file_read_instruction(self) -> str:
|
|
27741
29340
|
"""Short instruction for models: read the plan file instead of embedding full plan text."""
|
|
29341
|
+
# Find active step id for parent_step_id linkage
|
|
29342
|
+
bb = self._ensure_blackboard()
|
|
29343
|
+
active_step_id = ""
|
|
29344
|
+
active_step_idx = 0
|
|
29345
|
+
for t in bb.get("project_todos", []):
|
|
29346
|
+
if isinstance(t, dict) and t.get("category") == "plan_step" and t.get("status") == "in_progress":
|
|
29347
|
+
active_step_id = str(t.get("id", "") or "")
|
|
29348
|
+
active_step_idx = int(t.get("plan_step_index", 0) or 0) + 1
|
|
29349
|
+
break
|
|
29350
|
+
todo_note = ""
|
|
29351
|
+
if active_step_id:
|
|
29352
|
+
todo_note = (
|
|
29353
|
+
f"\nTODO UPDATE: Call TodoWrite at the START to set subtasks for the current step (Step {active_step_idx}) ONLY.\n"
|
|
29354
|
+
f"Each subtask MUST include parent_step_id='{active_step_id}'.\n"
|
|
29355
|
+
f"Create 3-5 subtasks that break down ONLY the current step's work.\n"
|
|
29356
|
+
f"Do NOT create subtasks for other plan steps. Mark each subtask completed as you finish it.\n"
|
|
29357
|
+
)
|
|
27742
29358
|
return (
|
|
27743
29359
|
f"[plan-file] The approved execution plan is at `{PLAN_FILE_RELATIVE_PATH}`.\n"
|
|
27744
29360
|
f"Use: read_file {PLAN_FILE_RELATIVE_PATH} to review full steps and live status.\n"
|
|
27745
29361
|
"The plan file is the authoritative source for step ordering and completion status.\n"
|
|
27746
29362
|
"Execute steps IN ORDER. Do NOT skip ahead. Mark current step done before advancing.\n"
|
|
27747
29363
|
"If a step references a skill or workflow, call load_skill to load it before proceeding."
|
|
29364
|
+
f"{todo_note}"
|
|
27748
29365
|
)
|
|
27749
29366
|
|
|
29367
|
+
@staticmethod
|
|
29368
|
+
def _group_plan_steps(raw_steps: list) -> list[str]:
|
|
29369
|
+
"""Group flat step array by major step number (first digit of N.N).
|
|
29370
|
+
|
|
29371
|
+
All 1.x sub-steps merge into one logical step, all 2.x into another, etc.
|
|
29372
|
+
If a step has the format "N. Summary title" (single digit, no sub-number),
|
|
29373
|
+
it becomes the group header. Otherwise a header is synthesized from the
|
|
29374
|
+
first sub-step.
|
|
29375
|
+
|
|
29376
|
+
Handles LLM output where sub-steps may be separate array elements OR
|
|
29377
|
+
concatenated in one string with spaces/newlines.
|
|
29378
|
+
|
|
29379
|
+
Returns a list where each element is one major step (multi-line string):
|
|
29380
|
+
"1. Summary Title\\n1.1 Sub-step A\\nDetail...\\n1.2 Sub-step B\\n..."
|
|
29381
|
+
"""
|
|
29382
|
+
import re
|
|
29383
|
+
if not raw_steps or not isinstance(raw_steps, list):
|
|
29384
|
+
return list(raw_steps or [])
|
|
29385
|
+
# Patterns
|
|
29386
|
+
sub_step_re = re.compile(r"^(\d+)\.(\d+)\s") # "N.M ..."
|
|
29387
|
+
major_step_re = re.compile(r"^(\d+)\.\s") # "N. ..." (summary line)
|
|
29388
|
+
mid_numbered_re = re.compile(r"(?<=\S)\s+(\d+\.\d+\s)")
|
|
29389
|
+
# Phase 0: Normalize — split mid-string N.N onto own lines
|
|
29390
|
+
normalized: list[str] = []
|
|
29391
|
+
for s in raw_steps:
|
|
29392
|
+
text = str(s or "").strip()
|
|
29393
|
+
if not text:
|
|
29394
|
+
continue
|
|
29395
|
+
fixed = mid_numbered_re.sub(r"\n\1", text)
|
|
29396
|
+
for line in fixed.split("\n"):
|
|
29397
|
+
stripped = line.strip()
|
|
29398
|
+
if stripped:
|
|
29399
|
+
normalized.append(stripped)
|
|
29400
|
+
if not normalized:
|
|
29401
|
+
return [str(s) for s in raw_steps if str(s or "").strip()]
|
|
29402
|
+
# Phase 1: Check if any N.N sub-step headers exist
|
|
29403
|
+
has_sub_steps = any(sub_step_re.match(ln) for ln in normalized)
|
|
29404
|
+
if not has_sub_steps:
|
|
29405
|
+
return normalized
|
|
29406
|
+
# Phase 2: Group by major number (first digit of N.N)
|
|
29407
|
+
from collections import OrderedDict
|
|
29408
|
+
major_groups: OrderedDict[str, list[str]] = OrderedDict()
|
|
29409
|
+
major_headers: dict[str, str] = {} # major_num → "N. Summary" if provided
|
|
29410
|
+
orphan_lines: list[str] = []
|
|
29411
|
+
current_major: str = ""
|
|
29412
|
+
for line in normalized:
|
|
29413
|
+
m_sub = sub_step_re.match(line)
|
|
29414
|
+
m_major = major_step_re.match(line)
|
|
29415
|
+
if m_sub:
|
|
29416
|
+
major_num = m_sub.group(1)
|
|
29417
|
+
current_major = major_num
|
|
29418
|
+
if major_num not in major_groups:
|
|
29419
|
+
major_groups[major_num] = []
|
|
29420
|
+
major_groups[major_num].append(line)
|
|
29421
|
+
elif m_major:
|
|
29422
|
+
# "N. Summary title" line → store as header for this major group
|
|
29423
|
+
major_num = m_major.group(1)
|
|
29424
|
+
current_major = major_num
|
|
29425
|
+
major_headers[major_num] = line
|
|
29426
|
+
if major_num not in major_groups:
|
|
29427
|
+
major_groups[major_num] = []
|
|
29428
|
+
else:
|
|
29429
|
+
# Detail line → attach to current major group
|
|
29430
|
+
if current_major and current_major in major_groups:
|
|
29431
|
+
major_groups[current_major].append(line)
|
|
29432
|
+
else:
|
|
29433
|
+
orphan_lines.append(line)
|
|
29434
|
+
# Phase 3: Build output — each major group becomes one step
|
|
29435
|
+
result: list[str] = []
|
|
29436
|
+
if orphan_lines:
|
|
29437
|
+
result.append("\n".join(orphan_lines))
|
|
29438
|
+
for major_num, lines in major_groups.items():
|
|
29439
|
+
if not lines:
|
|
29440
|
+
continue
|
|
29441
|
+
header = major_headers.get(major_num, "")
|
|
29442
|
+
if not header:
|
|
29443
|
+
# Synthesize header from first sub-step: "N. <first sub-step title>"
|
|
29444
|
+
first_sub = lines[0]
|
|
29445
|
+
m = sub_step_re.match(first_sub)
|
|
29446
|
+
if m:
|
|
29447
|
+
# Extract title part after "N.M "
|
|
29448
|
+
title_part = first_sub[m.end():].split("\n")[0].strip()
|
|
29449
|
+
# Remove detail after title (heuristic: title is before first Chinese/action verb)
|
|
29450
|
+
header = f"{major_num}. {title_part}" if title_part else f"{major_num}. Step {major_num}"
|
|
29451
|
+
else:
|
|
29452
|
+
header = f"{major_num}. Step {major_num}"
|
|
29453
|
+
result.append(header + "\n" + "\n".join(lines))
|
|
29454
|
+
return result
|
|
29455
|
+
|
|
27750
29456
|
# ── (legacy) _format_plan_proposal_markdown ──────────────────────
|
|
27751
29457
|
|
|
27752
29458
|
def _format_plan_proposal_markdown(self, proposal: dict) -> str:
|
|
@@ -27898,16 +29604,20 @@ class SessionState:
|
|
|
27898
29604
|
self.runtime_complexity_floor = str(self.runtime_task_complexity or "complex")
|
|
27899
29605
|
self.runtime_task_level_floor = int(self.runtime_task_level or 4)
|
|
27900
29606
|
# Auto-create todos from plan steps → write into bb["project_todos"]
|
|
27901
|
-
steps = chosen.get("steps", [])
|
|
29607
|
+
steps = self._group_plan_steps(chosen.get("steps", []))
|
|
27902
29608
|
if steps and isinstance(steps, list):
|
|
27903
29609
|
plan_todos = []
|
|
27904
29610
|
for i, step in enumerate(steps):
|
|
27905
|
-
step_text = trim(str(step or "").strip(),
|
|
29611
|
+
step_text = trim(str(step or "").strip(), 1500)
|
|
27906
29612
|
if not step_text:
|
|
27907
29613
|
continue
|
|
29614
|
+
# Extract header (first line) for todo display; keep full text for plan.md rendering
|
|
29615
|
+
step_lines = step_text.split("\n")
|
|
29616
|
+
step_header = step_lines[0].strip()
|
|
27908
29617
|
plan_todos.append({
|
|
27909
29618
|
"id": f"pt:{i:03d}",
|
|
27910
|
-
"content":
|
|
29619
|
+
"content": step_header,
|
|
29620
|
+
"full_content": step_text,
|
|
27911
29621
|
"status": "in_progress" if i == 0 else "pending",
|
|
27912
29622
|
"category": "plan_step",
|
|
27913
29623
|
"plan_step_index": i,
|
|
@@ -30228,6 +31938,11 @@ class SessionManager:
|
|
|
30228
31938
|
return bool(endpoint or complete_chat_endpoint(base))
|
|
30229
31939
|
if provider == "custom_http":
|
|
30230
31940
|
return bool(str(profile.get("endpoint", "") or "").strip())
|
|
31941
|
+
if provider == "anthropic":
|
|
31942
|
+
api_key = str(profile.get("api_key", "") or "").strip()
|
|
31943
|
+
endpoint = str(profile.get("endpoint", "") or "").strip()
|
|
31944
|
+
base = str(profile.get("base_url", "") or "").strip()
|
|
31945
|
+
return bool(api_key and (endpoint or base))
|
|
30231
31946
|
return False
|
|
30232
31947
|
|
|
30233
31948
|
def _option_is_runnable(self, option: dict) -> bool:
|
|
@@ -30604,16 +32319,22 @@ window.MathJax={
|
|
|
30604
32319
|
<label id="llmProviderLabel">Provider</label>
|
|
30605
32320
|
<select id="llmProvider">
|
|
30606
32321
|
<option value="ollama">Ollama</option>
|
|
32322
|
+
<option value="vllm">vLLM (Local)</option>
|
|
32323
|
+
<option value="lmstudio">LM Studio (Local)</option>
|
|
30607
32324
|
<option value="openai_compat">OpenAI Compatible</option>
|
|
32325
|
+
<option value="anthropic">Anthropic</option>
|
|
32326
|
+
<option value="glm">GLM</option>
|
|
32327
|
+
<option value="kimi">KIMI (Moonshot)</option>
|
|
32328
|
+
<option value="openrouter">OpenRouter</option>
|
|
30608
32329
|
<option value="siliconflow">SiliconFlow</option>
|
|
30609
32330
|
<option value="custom_http">Custom HTTP</option>
|
|
30610
32331
|
</select>
|
|
30611
32332
|
</div>
|
|
30612
32333
|
<div id="llmFieldsContainer"></div>
|
|
30613
32334
|
</div>
|
|
30614
|
-
<div class="llm-modal-footer">
|
|
32335
|
+
<div class="llm-modal-footer" style="flex-direction:column;gap:8px">
|
|
32336
|
+
<label for="configInput" id="llmConfigImport" class="llm-modal-btn-secondary" style="cursor:pointer;margin:0;text-align:center">Import config</label>
|
|
30615
32337
|
<button id="llmConfigConfirm" type="button" class="llm-modal-btn-primary">Confirm</button>
|
|
30616
|
-
<label for="configInput" id="llmConfigImport" class="llm-modal-btn-secondary" style="cursor:pointer;margin:0">Import config</label>
|
|
30617
32338
|
</div>
|
|
30618
32339
|
</div>
|
|
30619
32340
|
</div>
|
|
@@ -31146,9 +32867,10 @@ const CODE_KEYWORDS={default:new Set(['if','else','for','while','switch','case',
|
|
|
31146
32867
|
S.staticMode=STATIC_UI;
|
|
31147
32868
|
async function setTaskLevel(level){if(!S.activeId)return;const lvl=parseInt(level,10);try{await api('/api/sessions/'+S.activeId+'/config/task-level',{method:'POST',body:JSON.stringify({level:lvl})});updateLevelBtn(lvl);scheduleSnapshot({forceFull:false,delayMs:80,allowWhenFrozen:true})}catch(err){showError(err.message||String(err))}}
|
|
31148
32869
|
function updateLevelBtn(level){const btn=E('levelBtn');if(!btn)return;if(!level||level===0){btn.textContent=t('btn_level')+': '+t('level_auto')}else{const labels={1:'L1',2:'L2',3:'L3',4:'L4',5:'L5'};btn.textContent=t('btn_level')+': '+(labels[level]||t('level_auto'))}}
|
|
31149
|
-
const LLM_PROVIDER_FIELDS={ollama:[{key:'ollama_url',label:'Ollama URL',type:'url',placeholder:'http://127.0.0.1:11434',hint:'Ollama API endpoint'}],openai_compat:[{key:'openai_url',label:'API Base URL',type:'url',placeholder:'https://api.openai.com/v1',hint:'OpenAI-compatible endpoint'},{key:'openai_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'Your API key'},{key:'openai_model',label:'Model',type:'text',placeholder:'gpt-4o-mini',hint:'e.g. gpt-4o, claude-sonnet-4-20250514'}],siliconflow:[{key:'siliconflow_url',label:'API URL',type:'url',placeholder:'https://api.siliconflow.cn/v1',hint:'SiliconFlow API endpoint'},{key:'siliconflow_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'SiliconFlow API key'},{key:'siliconflow_model',label:'Model',type:'text',placeholder:'Qwen/Qwen3-Next-80B-A3B-Instruct',hint:'Model identifier'}],custom_http:[{key:'custom_url',label:'API Endpoint URL',type:'url',placeholder:'https://your-api.com/v1/chat/completions',hint:'Full API endpoint URL'},{key:'custom_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'API key (optional)'},{key:'custom_model',label:'Model',type:'text',placeholder:'model-name',hint:'Model identifier'},{key:'custom_headers',label:'Custom Headers (JSON)',type:'textarea',placeholder:'{"Authorization":"Bearer token","X-Custom":"value"}',hint:'JSON object of additional HTTP headers'},{key:'custom_payload',label:'Payload Template (JSON)',type:'textarea',placeholder:'{"custom_param":"value","stream":true}',hint:'Extra fields merged into the request body'},{key:'temperature',label:'Temperature',type:'number',placeholder:'0.2',hint:'0.0-2.0, lower=deterministic'},{key:'request_timeout',label:'Request Timeout (seconds)',type:'number',placeholder:'3600',hint:'Max seconds per LLM request'}]};
|
|
31150
|
-
function renderLlmFields(provider){const container=E('llmFieldsContainer');if(!container)return;let html='';if(provider==='ollama'){const fields=LLM_PROVIDER_FIELDS.ollama;for(const f of fields){html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><input type=\"'+f.type+'\" id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" value=\"\"><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}html+='<div class=\"llm-field\"><label>'+esc(t('llm_model'))+'</label><div style=\"display:flex;gap:8px;align-items:center\"><select id=\"llmF_ollama_model\" style=\"flex:1\"><option value=\"\">-- '+esc(t('llm_scan_first'))+' --</option></select><button type=\"button\" id=\"ollamaScanBtn\" class=\"llm-modal-btn-secondary\" style=\"flex:none;padding:6px 12px\">'+esc(t('llm_scan'))+'</button></div><div class=\"llm-hint\" id=\"ollamaScanHint\">'+esc(t('llm_scan_hint'))+'</div></div>'}else{const fields=LLM_PROVIDER_FIELDS[provider]||[];for(const f of fields){if(f.type==='textarea'){html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><textarea id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" rows=\"3\" style=\"width:100%;padding:8px 10px;border:1px solid var(--line,#d9e1ec);border-radius:8px;font-size:.84rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;resize:vertical;box-sizing:border-box\"></textarea><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}else{html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><input type=\"'+(f.type==='number'?'text':f.type)+'\" id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" value=\"\"><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}}}html+='<div class=\"llm-field\"><label>'+esc(t('llm_thinking_stream'))+'</label><select id=\"llmF_thinking_stream\"><option value=\"true\">'+esc(t('llm_enabled'))+'</option><option value=\"false\">'+esc(t('llm_disabled'))+'</option></select></div>';container.innerHTML=html;if(provider==='ollama'){const scanBtn=E('ollamaScanBtn');if(scanBtn)scanBtn.onclick=()=>scanOllamaModels()}}
|
|
32870
|
+
const LLM_PROVIDER_FIELDS={ollama:[{key:'ollama_url',label:'Ollama URL',type:'url',placeholder:'http://127.0.0.1:11434',hint:'Ollama API endpoint'}],vllm:[{key:'vllm_url',label:'vLLM URL',type:'url',placeholder:'http://localhost:8000/v1',hint:'vLLM OpenAI-compat endpoint'},{key:'vllm_model',label:'Model',type:'text',placeholder:'(auto-detect)',hint:'Leave empty to auto-detect'},{key:'vllm_key',label:'API Key (optional)',type:'password',placeholder:'',hint:'Usually not required for local'}],lmstudio:[{key:'lmstudio_url',label:'LM Studio URL',type:'url',placeholder:'http://localhost:1234/v1',hint:'LM Studio server endpoint'},{key:'lmstudio_model',label:'Model',type:'text',placeholder:'(auto-detect)',hint:'Leave empty to auto-detect'}],openai_compat:[{key:'openai_url',label:'API Base URL',type:'url',placeholder:'https://api.openai.com/v1',hint:'OpenAI-compatible endpoint'},{key:'openai_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'Your API key'},{key:'openai_model',label:'Model',type:'text',placeholder:'gpt-4o-mini',hint:'e.g. gpt-4o, claude-sonnet-4-20250514'}],anthropic:[{key:'anthropic_url',label:'API URL',type:'url',placeholder:'https://api.anthropic.com',hint:'Anthropic API endpoint'},{key:'anthropic_key',label:'API Key',type:'password',placeholder:'sk-ant-...',hint:'Anthropic API key'},{key:'anthropic_model',label:'Model',type:'text',placeholder:'claude-sonnet-4-20250514',hint:'e.g. claude-sonnet-4-20250514, claude-opus-4-20250514'}],glm:[{key:'glm_url',label:'API URL',type:'url',placeholder:'https://open.bigmodel.cn/api/paas/v4',hint:'GLM API endpoint'},{key:'glm_key',label:'API Key',type:'password',placeholder:'',hint:'GLM API Key'},{key:'glm_model',label:'Model',type:'text',placeholder:'glm-4-flash',hint:'e.g. glm-4-flash, glm-4-plus, glm-4v'}],kimi:[{key:'kimi_url',label:'API URL',type:'url',placeholder:'https://api.moonshot.cn/v1',hint:'KIMI/Moonshot API endpoint'},{key:'kimi_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'Moonshot API Key'},{key:'kimi_model',label:'Model',type:'text',placeholder:'moonshot-v1-8k',hint:'e.g. moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k'}],openrouter:[{key:'openrouter_url',label:'API URL',type:'url',placeholder:'https://openrouter.ai/api/v1',hint:'OpenRouter endpoint'},{key:'openrouter_key',label:'API Key',type:'password',placeholder:'sk-or-...',hint:'OpenRouter API Key'},{key:'openrouter_model',label:'Model',type:'text',placeholder:'meta-llama/llama-3.1-8b-instruct',hint:'Full model slug from openrouter.ai/models'}],siliconflow:[{key:'siliconflow_url',label:'API URL',type:'url',placeholder:'https://api.siliconflow.cn/v1',hint:'SiliconFlow API endpoint'},{key:'siliconflow_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'SiliconFlow API key'},{key:'siliconflow_model',label:'Model',type:'text',placeholder:'Qwen/Qwen3-Next-80B-A3B-Instruct',hint:'Model identifier'}],custom_http:[{key:'custom_url',label:'API Endpoint URL',type:'url',placeholder:'https://your-api.com/v1/chat/completions',hint:'Full API endpoint URL'},{key:'custom_key',label:'API Key',type:'password',placeholder:'sk-...',hint:'API key (optional)'},{key:'custom_model',label:'Model',type:'text',placeholder:'model-name',hint:'Model identifier'},{key:'custom_headers',label:'Custom Headers (JSON)',type:'textarea',placeholder:'{"Authorization":"Bearer token","X-Custom":"value"}',hint:'JSON object of additional HTTP headers'},{key:'custom_payload',label:'Payload Template (JSON)',type:'textarea',placeholder:'{"custom_param":"value","stream":true}',hint:'Extra fields merged into the request body'},{key:'temperature',label:'Temperature',type:'number',placeholder:'0.2',hint:'0.0-2.0, lower=deterministic'},{key:'request_timeout',label:'Request Timeout (seconds)',type:'number',placeholder:'3600',hint:'Max seconds per LLM request'}]};
|
|
32871
|
+
function renderLlmFields(provider){const container=E('llmFieldsContainer');if(!container)return;let html='';const _localScanProviders=['ollama','vllm','lmstudio'];if(provider==='ollama'){const fields=LLM_PROVIDER_FIELDS.ollama;for(const f of fields){html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><input type=\"'+f.type+'\" id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" value=\"\"><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}html+='<div class=\"llm-field\"><label>'+esc(t('llm_model'))+'</label><div style=\"display:flex;gap:8px;align-items:center\"><select id=\"llmF_ollama_model\" style=\"flex:1\"><option value=\"\">-- '+esc(t('llm_scan_first'))+' --</option></select><button type=\"button\" id=\"ollamaScanBtn\" class=\"llm-modal-btn-secondary\" style=\"flex:none;padding:6px 12px\">'+esc(t('llm_scan'))+'</button></div><div class=\"llm-hint\" id=\"ollamaScanHint\">'+esc(t('llm_scan_hint'))+'</div></div>'}else if(provider==='vllm'||provider==='lmstudio'){const fields=LLM_PROVIDER_FIELDS[provider]||[];for(const f of fields){html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><input type=\"'+(f.type==='number'?'text':f.type)+'\" id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" value=\"\"><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}const urlKey=provider==='vllm'?'vllm_url':'lmstudio_url';const modelKey=provider==='vllm'?'vllm_model':'lmstudio_model';html+='<div class=\"llm-field\"><div style=\"display:flex;gap:8px;align-items:center\"><button type=\"button\" id=\"localScanBtn\" class=\"llm-modal-btn-secondary\" style=\"flex:none;padding:6px 12px\">'+esc(t('llm_scan'))+'</button></div><div class=\"llm-hint\" id=\"localScanHint\">'+esc(t('llm_scan_hint'))+'</div></div>'}else{const fields=LLM_PROVIDER_FIELDS[provider]||[];for(const f of fields){if(f.type==='textarea'){html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><textarea id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" rows=\"3\" style=\"width:100%;padding:8px 10px;border:1px solid var(--line,#d9e1ec);border-radius:8px;font-size:.84rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;resize:vertical;box-sizing:border-box\"></textarea><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}else{html+='<div class=\"llm-field\"><label>'+esc(f.label)+'</label><input type=\"'+(f.type==='number'?'text':f.type)+'\" id=\"llmF_'+f.key+'\" placeholder=\"'+esc(f.placeholder||'')+'\" value=\"\"><div class=\"llm-hint\">'+esc(f.hint||'')+'</div></div>'}}}html+='<div class=\"llm-field\"><label>'+esc(t('llm_thinking_stream'))+'</label><select id=\"llmF_thinking_stream\"><option value=\"true\">'+esc(t('llm_enabled'))+'</option><option value=\"false\">'+esc(t('llm_disabled'))+'</option></select></div>';container.innerHTML=html;if(provider==='ollama'){const scanBtn=E('ollamaScanBtn');if(scanBtn)scanBtn.onclick=()=>scanOllamaModels()}if(provider==='vllm'||provider==='lmstudio'){const scanBtn=E('localScanBtn');if(scanBtn)scanBtn.onclick=()=>scanOpenAICompatModels(provider)}}
|
|
31151
32872
|
async function scanOllamaModels(){const urlEl=E('llmF_ollama_url');const sel=E('llmF_ollama_model');const hint=E('ollamaScanHint');const baseUrl=(urlEl?.value||'').trim()||'http://127.0.0.1:11434';if(hint)hint.textContent=t('llm_scanning');try{const res=await fetch('/api/ollama/models?base_url='+encodeURIComponent(baseUrl));const data=await res.json();if(!data.ok||!data.models?.length){if(hint)hint.textContent=t('llm_scan_empty')+(data.error?' ('+data.error+')':'');return}if(sel){sel.innerHTML='';for(const m of data.models){const op=document.createElement('option');op.value=m;op.textContent=m;sel.appendChild(op)}}if(hint)hint.textContent=t('llm_scan_found').replace('{n}',String(data.models.length))}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
|
|
32873
|
+
async function scanOpenAICompatModels(provider){const urlKey=provider==='vllm'?'vllm_url':'lmstudio_url';const modelKey=provider==='vllm'?'vllm_model':'lmstudio_model';const urlEl=E('llmF_'+urlKey);const modelEl=E('llmF_'+modelKey);const hint=E('localScanHint');const defaults={vllm:'http://localhost:8000/v1',lmstudio:'http://localhost:1234/v1'};const baseUrl=(urlEl?.value||'').trim()||defaults[provider]||'';const apiKey=(E('llmF_'+provider+'_key')?.value||'').trim();if(hint)hint.textContent=t('llm_scanning');try{let url='/api/openai_compat/models?base_url='+encodeURIComponent(baseUrl);if(apiKey)url+='&api_key='+encodeURIComponent(apiKey);const res=await fetch(url);const data=await res.json();if(!data.ok||!data.models?.length){if(hint)hint.textContent=t('llm_scan_empty')+(data.error?' ('+data.error+')':'');return}if(modelEl){modelEl.value=data.models[0]}if(hint)hint.textContent=t('llm_scan_found').replace('{n}',String(data.models.length))+': '+data.models.slice(0,3).join(', ')}catch(err){if(hint)hint.textContent=t('llm_scan_error')+': '+(err.message||String(err))}}
|
|
31152
32874
|
function collectLlmConfig(){const provider=E('llmProvider')?.value||'ollama';const config={provider:provider};if(provider==='ollama'){config.ollama_url=(E('llmF_ollama_url')?.value||'').trim()||'http://127.0.0.1:11434';config.ollama_model=E('llmF_ollama_model')?.value||''}else if(provider==='custom_http'){const fields=LLM_PROVIDER_FIELDS.custom_http;for(const f of fields){const el=E('llmF_'+f.key);if(!el)continue;if(f.type==='textarea'){config[f.key]=el.value.trim()}else if(f.key==='temperature'){const v=parseFloat(el.value);if(!isNaN(v))config[f.key]=v}else if(f.key==='request_timeout'){const v=parseInt(el.value,10);if(!isNaN(v)&&v>0)config[f.key]=v}else{config[f.key]=el.value.trim()}}}else{const fields=LLM_PROVIDER_FIELDS[provider]||[];for(const f of fields){const el=E('llmF_'+f.key);if(el)config[f.key]=el.value.trim()}}config.thinking_stream=E('llmF_thinking_stream')?.value==='true';return config}
|
|
31153
32875
|
async function submitLlmConfig(){if(!S.activeId){showError(t('select_session_first'));return}const config=collectLlmConfig();try{const payload={filename:'LLM.config.json',mime:'application/json',content_b64:btoa(unescape(encodeURIComponent(JSON.stringify(config,null,2))))};const out=await api('/api/sessions/'+S.activeId+'/uploads',{method:'POST',body:JSON.stringify(payload)});if(!out?.model_catalog){showError(t('config_uploaded_no_profiles'))}else{showError('')}const cat=out?.model_catalog||await loadModelCatalog();if(!applyModelCatalog(cat)){renderModelControls()}await refreshSnapshot({forceFull:true,allowWhenFrozen:true});E('llmConfigModal').style.display='none'}catch(err){showError(err.message||String(err))}}
|
|
31154
32876
|
function openLlmConfigModal(){const modal=E('llmConfigModal');if(!modal)return;modal.style.display='flex';const prov=E('llmProvider');if(prov){renderLlmFields(prov.value)}}
|
|
@@ -43716,6 +45438,25 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
43716
45438
|
return self._send_json({"ok": True, "models": models, "base_url": ollama_url})
|
|
43717
45439
|
except Exception as exc:
|
|
43718
45440
|
return self._send_json({"ok": False, "models": [], "error": str(exc)[:300], "base_url": ollama_url})
|
|
45441
|
+
if path == "/api/openai_compat/models":
|
|
45442
|
+
base_url = str((query.get("base_url", [""]) or [""])[0]).strip()
|
|
45443
|
+
api_key = str((query.get("api_key", [""]) or [""])[0]).strip()
|
|
45444
|
+
if not base_url:
|
|
45445
|
+
return self._send_json({"ok": False, "models": [], "error": "base_url required"})
|
|
45446
|
+
try:
|
|
45447
|
+
import urllib.request
|
|
45448
|
+
models_url = base_url.rstrip("/") + "/models"
|
|
45449
|
+
req = urllib.request.Request(models_url, method="GET")
|
|
45450
|
+
req.add_header("Accept", "application/json")
|
|
45451
|
+
if api_key:
|
|
45452
|
+
req.add_header("Authorization", f"Bearer {api_key}")
|
|
45453
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
45454
|
+
raw = json.loads(resp.read().decode("utf-8"))
|
|
45455
|
+
data = raw.get("data", [])
|
|
45456
|
+
model_ids = [str(m.get("id", "")) for m in data if isinstance(m, dict) and m.get("id")]
|
|
45457
|
+
return self._send_json({"ok": True, "models": model_ids, "base_url": base_url})
|
|
45458
|
+
except Exception as exc:
|
|
45459
|
+
return self._send_json({"ok": False, "models": [], "error": str(exc)[:300], "base_url": base_url})
|
|
43719
45460
|
if path == "/api/skills":
|
|
43720
45461
|
return self._send_json(self.app.skills_catalog())
|
|
43721
45462
|
if path == "/api/skills/providers":
|
|
@@ -45001,8 +46742,10 @@ def main():
|
|
|
45001
46742
|
startup_tags = list_ollama_models(bootstrap_base_url)
|
|
45002
46743
|
if startup_tags:
|
|
45003
46744
|
resolved_model = bootstrap_model if bootstrap_model in startup_tags else startup_tags[0]
|
|
46745
|
+
print(f"[web-agent] ollama: found {len(startup_tags)} model(s) at {bootstrap_base_url} — using '{resolved_model}'")
|
|
45004
46746
|
else:
|
|
45005
46747
|
resolved_model = bootstrap_model
|
|
46748
|
+
print(f"[web-agent] ollama: not available at {bootstrap_base_url} — using fallback '{resolved_model}'")
|
|
45006
46749
|
resolved_thinking = False
|
|
45007
46750
|
requested_ctx_limit = int(args.ctx_limit or TOKEN_THRESHOLD)
|
|
45008
46751
|
resolved_ctx_limit = max(
|