Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions apps/api/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import json

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

Expand Down Expand Up @@ -49,20 +51,22 @@ def health():

@app.get("/v1/tools")
def list_tools():
cli = mcp_mgr.get_client(server_id=settings.mcp_default_server_id)
tools = cli.list_tools()
return {
"tools": [
tool_list = []
for server_id in mcp_mgr.list_server_ids():
cli = mcp_mgr.get_client(server_id=server_id)
tools = cli.list_tools()
tool_list.extend(
{
"tool_id": t.name,
"name": t.name,
"risk": t.risk,
"description": t.description,
"input_schema": t.input_schema,
"server_id": server_id,
}
for t in tools
]
}
)
return {"tools": tool_list}


@app.post("/v1/workflows/draft")
Expand Down Expand Up @@ -91,3 +95,17 @@ def get_run(run_id: str):
if not rec:
raise HTTPException(status_code=404, detail="run not found")
return rec.model_dump()


@app.get("/v1/finetune/jobs/{job_id}")
def get_finetune_job(job_id: str):
cli = mcp_mgr.get_client(server_id="finetune")
payload = json.loads(cli.call_tool(name="tool.finetune.get_job_status", arguments={"job_id": job_id}))
return payload


@app.get("/v1/finetune/jobs/{job_id}/events")
def get_finetune_job_events(job_id: str, limit: int = 20):
cli = mcp_mgr.get_client(server_id="finetune")
payload = json.loads(cli.call_tool(name="tool.finetune.list_job_events", arguments={"job_id": job_id, "limit": limit}))
return payload
14 changes: 14 additions & 0 deletions apps/finetune_workflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Finetune Workflow (MVA)

This folder hosts the finetune workflow assets and template code used by the MCP finetune server.
It is designed to be standalone while relying on the core wevo runtime (agents, MCP tools, storage).

## Structure
- `templates/base/` — minimal finetune template files with placeholders.

## Placeholders
Templates use:
- `{{MODEL_ID}}`
- `{{DATASET_PATH}}`

The finetune MCP server copies the template into `data/finetune/runs/<run_id>/` and replaces placeholders.
14 changes: 14 additions & 0 deletions apps/finetune_workflow/templates/base/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"model_id": "{{MODEL_ID}}",
"dataset_path": "{{DATASET_PATH}}",
"output_dir": "./output",
"num_train_epochs": 2,
"per_device_train_batch_size": 2,
"gradient_accumulation_steps": 4,
"learning_rate": 2e-5,
"max_seq_length": 512,
"lora_r": 16,
"lora_alpha": 32,
"lora_dropout": 0.05,
"seed": 42
}
121 changes: 121 additions & 0 deletions apps/finetune_workflow/templates/base/train.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Finetune script template for functiongemma / qwen3 (real training path)."""
from __future__ import annotations

import argparse
import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional

from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
Trainer,
TrainingArguments,
DataCollatorForLanguageModeling,
)

try:
from peft import LoraConfig, get_peft_model, TaskType
except Exception as exc: # pragma: no cover
raise RuntimeError("peft is required for LoRA finetuning") from exc


@dataclass
class TrainConfig:
model_id: str
dataset_path: str
output_dir: str
num_train_epochs: int = 2
per_device_train_batch_size: int = 2
gradient_accumulation_steps: int = 4
learning_rate: float = 2e-5
max_seq_length: int = 512
lora_r: int = 16
lora_alpha: int = 32
lora_dropout: float = 0.05
seed: int = 42
text_field: Optional[str] = None


def load_config(path: str) -> TrainConfig:
raw = json.loads(Path(path).read_text(encoding="utf-8"))
return TrainConfig(**raw)


def format_sample(sample: Dict[str, Any]) -> str:
if "text" in sample and sample["text"]:
return str(sample["text"])
instruction = str(sample.get("instruction") or "")
input_text = str(sample.get("input") or "")
output_text = str(sample.get("output") or "")
parts = [p for p in [instruction, input_text, output_text] if p]
return "\n".join(parts)


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=str, required=True)
args = parser.parse_args()

cfg = load_config(args.config)
os.environ["TOKENIZERS_PARALLELISM"] = "false"

tokenizer = AutoTokenizer.from_pretrained(cfg.model_id, use_fast=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(cfg.model_id)

lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=cfg.lora_r,
lora_alpha=cfg.lora_alpha,
lora_dropout=cfg.lora_dropout,
)
model = get_peft_model(model, lora_config)

raw_dataset = load_dataset("json", data_files={"train": cfg.dataset_path})

def tokenize_fn(batch: Dict[str, List[str]]) -> Dict[str, Any]:
texts = [format_sample({"text": t}) for t in batch["text"]] if "text" in batch else [
format_sample({k: v[i] for k, v in batch.items()}) for i in range(len(next(iter(batch.values()))))
]
return tokenizer(texts, truncation=True, max_length=cfg.max_seq_length)

tokenized = raw_dataset.map(tokenize_fn, batched=True, remove_columns=raw_dataset["train"].column_names)

training_args = TrainingArguments(
output_dir=cfg.output_dir,
num_train_epochs=cfg.num_train_epochs,
per_device_train_batch_size=cfg.per_device_train_batch_size,
gradient_accumulation_steps=cfg.gradient_accumulation_steps,
learning_rate=cfg.learning_rate,
logging_steps=10,
save_steps=50,
save_total_limit=2,
seed=cfg.seed,
fp16=False,
report_to=[],
)

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized["train"],
data_collator=data_collator,
)

trainer.train()
trainer.save_model(cfg.output_dir)
tokenizer.save_pretrained(cfg.output_dir)

print(f"Training complete. Saved to {cfg.output_dir}")


if __name__ == "__main__":
main()
11 changes: 11 additions & 0 deletions core/agents/central_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
- 生成 1 个节点:
- n1: agent_specs/weather_agent.json,tool_scopes: ["tool.weather.get"]
- trigger.input_mapping 至少包含 city (string),没给则默认 "Paris"

3) LLM 微调/finetune(关键词包含:微调/finetune/训练模型/小模型)
- 使用系统内置 finetune archetype(不要自己编造复杂 JSON 结构)
"""

TASK_INTENT_SCHEMA_HINT = """你是企业工作流平台的【Workflow Compiler - Understand 阶段】。
Expand Down Expand Up @@ -126,6 +129,12 @@ def _looks_like_incident_intent(text: str) -> bool:
keywords = ("incident", "告警", "故障", "宕机", "异常", "安全事件", "报警", "triage")
return any(k.lower() in t for k in keywords)

@staticmethod
def _looks_like_finetune_intent(text: str) -> bool:
t = (text or "").lower()
keywords = ("finetune", "微调", "训练模型", "小模型", "模型训练")
return any(k.lower() in t for k in keywords)

def _draft_from_archetype(self, *, archetype_filename: str, user_intent: str) -> WorkflowDefinition:
version = datetime.datetime.utcnow().strftime("%Y.%m.%d+1")
tmpl = self._load_archetype_json(archetype_filename)
Expand Down Expand Up @@ -569,6 +578,8 @@ def draft_workflow(self, intent: str) -> WorkflowDefinition:
return self._draft_from_archetype(archetype_filename="onboarding_digital_worker_v1.json", user_intent=intent)
if self._looks_like_incident_intent(intent):
return self._draft_from_archetype(archetype_filename="incident_triage_v1.json", user_intent=intent)
if self._looks_like_finetune_intent(intent):
return self._draft_from_archetype(archetype_filename="finetune_llm_auto_v1.json", user_intent=intent)
except Exception:
# Fall back to the baseline/LLM path below
pass
Expand Down
8 changes: 5 additions & 3 deletions core/agents/node_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,12 @@ def run_agent_node(
mcp_bundle = build_ollama_tools([])
tool_funcs = []
else:
client = self.mcp.get_client(server_id=settings.mcp_default_server_id)
tools_by_name_server = self.catalog.refresh(client=client)
tools_by_server: Dict[str, Dict[str, ToolMeta]] = {}
for server_id in self.mcp.list_server_ids():
client = self.mcp.get_client(server_id=server_id)
tools_by_server[client.server_id] = self.catalog.refresh(client=client)
allowed_tools = self.catalog.get_allowed_tools(
tools_by_server={client.server_id: tools_by_name_server},
tools_by_server=tools_by_server,
allowed_tool_ids=list(allowed),
)
mcp_bundle = build_ollama_tools(allowed_tools)
Expand Down
14 changes: 12 additions & 2 deletions core/mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,18 @@ def default_mcp_servers() -> List[MCPServerConfig]:
"MCP_SERVER_ID": "acmetech",
},
cwd=str(root),
)
),
MCPServerConfig(
id="finetune",
transport="stdio",
command=[sys.executable, "-m", "mcp_servers.finetune_server"],
env={
"PYTHONUNBUFFERED": "1",
"MCP_SERVER_ID": "finetune",
"FINETUNE_DB_PATH": str(root / "data" / "finetune_jobs.sqlite"),
},
cwd=str(root),
),
]


Expand All @@ -56,4 +67,3 @@ def load_mcp_servers() -> List[MCPServerConfig]:
out.append(MCPServerConfig.model_validate(item))
return out


5 changes: 4 additions & 1 deletion core/mcp/server_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def __init__(self) -> None:
self._servers_by_id: Dict[str, MCPServerConfig] = {s.id: s for s in load_mcp_servers()}
self._handles: Dict[str, _ServerHandle] = {}

def list_server_ids(self) -> list[str]:
with self._lock:
return sorted(self._servers_by_id.keys())

def get_client(self, *, server_id: str) -> StdioMCPClient:
with self._lock:
if server_id in self._handles:
Expand Down Expand Up @@ -59,4 +63,3 @@ def shutdown(self) -> None:
pass
self._handles.pop(hid, None)


29 changes: 29 additions & 0 deletions data/agent_specs/finetune_monitor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"agent_id": "finetune_monitor_v1",
"role": "Monitor finetune job progress and run evaluation",
"model_profile": {
"provider": "local",
"model": "qwen3",
"temperature": 0.2
},
"system_prompt": "You are monitoring a finetune job. Use tools to check job status. If completed, run evaluation. Return STRICT JSON with keys job_id, status, stage, progress, evaluation (optional), next_check_after_s (if still running).",
"io": {
"input_schema_ref": "schemas/finetune_plan_output.json",
"output_schema_ref": "schemas/finetune_monitor_output.json"
},
"tools": {
"allowed": [
"tool.finetune.get_job_status",
"tool.finetune.wait_for_job",
"tool.finetune.list_job_events",
"tool.finetune.run_evaluation"
],
"require_confirmation": []
},
"quality": {
"checks": [
{"type": "json_only"}
]
},
"notes": "MVA finetune monitor agent spec."
}
36 changes: 36 additions & 0 deletions data/agent_specs/finetune_planner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"agent_id": "finetune_planner_v1",
"role": "Plan and kickoff a finetune job for user requirements",
"model_profile": {
"provider": "local",
"model": "qwen3",
"temperature": 0.3
},
"system_prompt": "You are a finetune workflow planner. Use tools to select a model, find/download data, prepare a template, and start a finetune job. If no dataset is found, generate synthetic data. If input_json.mock_mode is true, pass mock=true to finetune tools. If input_json.mock_mode is false, pass mock=false and use real dataset/model IDs. Return STRICT JSON with keys selected_model, dataset_strategy, dataset_path, run_id, run_dir, job_id, job_status, mock_mode, notes.",
"io": {
"input_schema_ref": "schemas/finetune_intake_input.json",
"output_schema_ref": "schemas/finetune_plan_output.json"
},
"tools": {
"allowed": [
"tool.finetune.list_models",
"tool.finetune.search_datasets",
"tool.finetune.download_dataset",
"tool.finetune.generate_synthetic_data",
"tool.finetune.prepare_template",
"tool.finetune.start_job"
],
"require_confirmation": [
"tool.finetune.start_job",
"tool.finetune.download_dataset",
"tool.finetune.generate_synthetic_data",
"tool.finetune.prepare_template"
]
},
"quality": {
"checks": [
{"type": "json_only"}
]
},
"notes": "MVA finetune planner agent spec."
}
14 changes: 14 additions & 0 deletions data/schemas/finetune_intake_input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Finetune Intake Input",
"type": "object",
"properties": {
"request_text": {"type": "string"},
"target_device": {"type": "string", "description": "device class e.g. mobile"},
"control_scope": {"type": "string", "description": "what the model should control"},
"dataset_preference": {"type": "string", "description": "preferred dataset keywords"},
"constraints": {"type": "array", "items": {"type": "string"}},
"mock_mode": {"type": "boolean", "description": "true=mock pipeline, false=real finetune"}
},
"required": ["request_text"]
}
14 changes: 14 additions & 0 deletions data/schemas/finetune_monitor_output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Finetune Monitor Output",
"type": "object",
"properties": {
"job_id": {"type": "string"},
"status": {"type": "string"},
"stage": {"type": "string"},
"progress": {"type": "number"},
"evaluation": {"type": "object"},
"next_check_after_s": {"type": "integer"}
},
"required": ["job_id", "status", "stage", "progress"]
}
Loading