|
|
@@ -38,16 +38,22 @@ class CreateAgentRequest(BaseModel):
|
|
|
config: Dict[str, Any]
|
|
|
tags: list = Field(default_factory=list)
|
|
|
environment: str = Field(default="production", max_length=64)
|
|
|
- agent_dir: str = Field(default="", description="Agent template directory path")
|
|
|
+ agent_dir: str = Field(default="", description="Agent template/system directory path")
|
|
|
agent_template: str = Field(default="", description="Agent template name (e.g. qaagent67wiki)")
|
|
|
instance_dir: str = Field(default="", description="Instance data directory for this agent")
|
|
|
kb_ids: list = Field(default_factory=list, description="Linked knowledge base IDs")
|
|
|
kb_search_mode: str = Field(default="bm25", description="bm25 | pageindex | fusion")
|
|
|
+ work_dir: str = Field(default="", description="工作产物目录: Bash tool CWD; 最终输出文件落这里")
|
|
|
+ source_dir: str = Field(default="", description="数据来源目录: 只读输入数据; 注入 system prompt 前缀")
|
|
|
+ run_dir: str = Field(default="", description="运行日志目录: run workspace/run_{ts}/ 的父目录; 若空则用 work_dir")
|
|
|
|
|
|
class UpdateAgentRequest(BaseModel):
|
|
|
config: Dict[str, Any]
|
|
|
changelog: str = ""
|
|
|
agent_dir: str = ""
|
|
|
+ work_dir: str = ""
|
|
|
+ source_dir: str = ""
|
|
|
+ run_dir: str = ""
|
|
|
kb_ids: Optional[list] = None
|
|
|
kb_search_mode: Optional[str] = None
|
|
|
|
|
|
@@ -128,12 +134,14 @@ async def create_agent(
|
|
|
now = now_utc()
|
|
|
db.execute(
|
|
|
"INSERT INTO agents (id, tenant_id, name, description, current_version, tags, environment, "
|
|
|
- "agent_dir, agent_template, instance_dir, kb_ids, kb_search_mode, status, created_at, updated_at) "
|
|
|
- "VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)",
|
|
|
+ "agent_dir, agent_template, instance_dir, kb_ids, kb_search_mode, "
|
|
|
+ "work_dir, source_dir, run_dir, status, created_at, updated_at) "
|
|
|
+ "VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)",
|
|
|
(agent_id, tenant.tenant_id, req.name, req.description,
|
|
|
json.dumps(req.tags), req.environment,
|
|
|
agent_dir, req.agent_template, instance_dir,
|
|
|
- json.dumps(req.kb_ids), req.kb_search_mode, now, now)
|
|
|
+ json.dumps(req.kb_ids), req.kb_search_mode,
|
|
|
+ req.work_dir, req.source_dir, req.run_dir, now, now)
|
|
|
)
|
|
|
db.execute(
|
|
|
"INSERT INTO agent_versions (agent_id, version, config, config_hash, changelog, created_by, created_at) "
|
|
|
@@ -221,24 +229,33 @@ async def update_agent(
|
|
|
"SELECT config_hash FROM agent_versions WHERE agent_id = ? ORDER BY version DESC LIMIT 1",
|
|
|
(agent_id,)
|
|
|
)
|
|
|
- if last_version and last_version["config_hash"] == config_hash:
|
|
|
- return {"message": "Config unchanged, no new version created", "version": agent["current_version"]}
|
|
|
+ config_changed = not (last_version and last_version["config_hash"] == config_hash)
|
|
|
|
|
|
- # Validate
|
|
|
- try:
|
|
|
- _compile_agent(req.config)
|
|
|
- except Exception as e:
|
|
|
- api_error(400, "INVALID_CONFIG", str(e))
|
|
|
-
|
|
|
- new_version = agent["current_version"] + 1
|
|
|
now = now_utc()
|
|
|
- db.execute(
|
|
|
- "INSERT INTO agent_versions (agent_id, version, config, config_hash, changelog, created_by, created_at) "
|
|
|
- "VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
|
- (agent_id, new_version, config_json, config_hash, req.changelog, tenant.user_id, now)
|
|
|
- )
|
|
|
- update_fields = "current_version = ?, agent_dir = ?, updated_at = ?"
|
|
|
- update_vals = [new_version, req.agent_dir, now]
|
|
|
+ new_version = agent["current_version"]
|
|
|
+
|
|
|
+ if config_changed:
|
|
|
+ # Validate before inserting a new version
|
|
|
+ try:
|
|
|
+ _compile_agent(req.config)
|
|
|
+ except Exception as e:
|
|
|
+ api_error(400, "INVALID_CONFIG", str(e))
|
|
|
+
|
|
|
+ new_version = agent["current_version"] + 1
|
|
|
+ db.execute(
|
|
|
+ "INSERT INTO agent_versions (agent_id, version, config, config_hash, changelog, created_by, created_at) "
|
|
|
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
|
+ (agent_id, new_version, config_json, config_hash, req.changelog, tenant.user_id, now)
|
|
|
+ )
|
|
|
+
|
|
|
+ # Always persist directory and metadata fields regardless of whether
|
|
|
+ # config changed — prior bug: early return on "config unchanged" meant
|
|
|
+ # work_dir / source_dir / run_dir edits were silently dropped.
|
|
|
+ update_fields = "agent_dir = ?, work_dir = ?, source_dir = ?, run_dir = ?, updated_at = ?"
|
|
|
+ update_vals = [req.agent_dir, req.work_dir, req.source_dir, req.run_dir, now]
|
|
|
+ if config_changed:
|
|
|
+ update_fields = "current_version = ?, " + update_fields
|
|
|
+ update_vals = [new_version] + update_vals
|
|
|
if req.kb_ids is not None:
|
|
|
update_fields += ", kb_ids = ?"
|
|
|
update_vals.append(json.dumps(req.kb_ids))
|
|
|
@@ -249,7 +266,10 @@ async def update_agent(
|
|
|
db.execute(f"UPDATE agents SET {update_fields} WHERE id = ?", tuple(update_vals))
|
|
|
db.commit()
|
|
|
|
|
|
- return {"agent_id": agent_id, "version": new_version, "updated_at": now}
|
|
|
+ if config_changed:
|
|
|
+ return {"agent_id": agent_id, "version": new_version, "updated_at": now}
|
|
|
+ return {"agent_id": agent_id, "version": new_version, "updated_at": now,
|
|
|
+ "message": "Config unchanged; directory/metadata updated"}
|
|
|
|
|
|
|
|
|
@router.delete("/{agent_id}")
|
|
|
@@ -389,6 +409,9 @@ async def run_agent(
|
|
|
# Resolve agent_dir: prefer instance_dir (per-instance data), fallback to agent_dir
|
|
|
instance_dir = agent.get("instance_dir", "") or ""
|
|
|
agent_dir = instance_dir or agent.get("agent_dir", "") or ""
|
|
|
+ work_dir = agent.get("work_dir", "") or ""
|
|
|
+ source_dir = agent.get("source_dir", "") or ""
|
|
|
+ run_dir = agent.get("run_dir", "") or ""
|
|
|
|
|
|
# KB context injection: enrich input with top-K retrieved passages
|
|
|
kb_ids = json.loads(agent.get("kb_ids") or "[]")
|
|
|
@@ -423,6 +446,7 @@ async def run_agent(
|
|
|
lambda: _execute_agent(
|
|
|
config, enriched_input, agent_dir=agent_dir, run_id=run_id,
|
|
|
continue_workspace=continue_workspace,
|
|
|
+ work_dir=work_dir, source_dir=source_dir, run_dir=run_dir,
|
|
|
),
|
|
|
)
|
|
|
duration_ms = int((time.time() - t0) * 1000)
|
|
|
@@ -512,6 +536,9 @@ async def run_agent_stream(
|
|
|
# Resolve agent_dir: prefer instance_dir, fallback to agent_dir
|
|
|
instance_dir = agent.get("instance_dir", "") or ""
|
|
|
agent_dir = instance_dir or agent.get("agent_dir", "") or ""
|
|
|
+ work_dir = agent.get("work_dir", "") or ""
|
|
|
+ source_dir = agent.get("source_dir", "") or ""
|
|
|
+ run_dir = agent.get("run_dir", "") or ""
|
|
|
|
|
|
# KB context injection for streaming run
|
|
|
kb_ids = json.loads(agent.get("kb_ids") or "[]")
|
|
|
@@ -715,6 +742,7 @@ async def run_agent_stream(
|
|
|
config, stream_input, on_step=event_queue.put,
|
|
|
agent_dir=agent_dir, run_id=run_id,
|
|
|
continue_workspace=continue_workspace,
|
|
|
+ work_dir=work_dir, source_dir=source_dir, run_dir=run_dir,
|
|
|
)
|
|
|
duration_ms = int((time.time() - t0) * 1000)
|
|
|
workspace_path = trace_info.get("workspace_path", "")
|
|
|
@@ -1425,24 +1453,49 @@ def _compile_agent(config: dict):
|
|
|
|
|
|
def _execute_agent(config: dict, input_text: str, on_step=None,
|
|
|
agent_dir: str = "", run_id: str = "",
|
|
|
- continue_workspace: str = ""):
|
|
|
+ continue_workspace: str = "", work_dir: str = "",
|
|
|
+ source_dir: str = "", run_dir: str = ""):
|
|
|
"""Execute agent via lambdagent. Returns (result, trace_info).
|
|
|
|
|
|
- Args:
|
|
|
- config: Agent YAML config dict.
|
|
|
- input_text: User prompt.
|
|
|
- on_step: Optional callback for streaming.
|
|
|
- agent_dir: Agent directory path — if set, creates workspace/run_{timestamp}/.
|
|
|
- run_id: Run ID for workspace metadata.
|
|
|
- continue_workspace: If set, reuse this existing workspace dir directly
|
|
|
- instead of creating a fresh one. Used when the user clicks
|
|
|
- "continue this run" — sub-agents see prior cycle_1/cycle_2/...
|
|
|
- files via the normal {workspace} path, not via prompt indirection.
|
|
|
+ 3-directory system
|
|
|
+ ─────────────────
|
|
|
+ source_dir : 数据来源目录. Read-only input data location.
|
|
|
+ When set, a preamble is injected into the system prompt informing
|
|
|
+ the agent where to find source documents/data.
|
|
|
+ work_dir : 工作产物目录. Final-output directory; shell Bash CWD.
|
|
|
+ The agent writes its deliverables here.
|
|
|
+ run_dir : 运行日志目录. Base for run workspace creation
|
|
|
+ (workspace/run_{ts}/ sub-dirs with traces, configs, intermediate
|
|
|
+ artifacts). Falls back to work_dir, then agent_dir.
|
|
|
+
|
|
|
+ Other args
|
|
|
+ ──────────
|
|
|
+ config: Agent YAML config dict.
|
|
|
+ input_text: User prompt.
|
|
|
+ on_step: Optional callback for streaming.
|
|
|
+ agent_dir: Agent template/system directory (fallback for run_dir).
|
|
|
+ run_id: Run ID for workspace metadata.
|
|
|
+ continue_workspace: If set, reuse this existing workspace dir directly.
|
|
|
"""
|
|
|
import yaml, os
|
|
|
from agentpaas.engine.sandbox import create_run_workspace, save_run_artifacts
|
|
|
|
|
|
- # 创建 run workspace(如果有 agent_dir)
|
|
|
+ # ── source_dir injection ──────────────────────────────────────────────────
|
|
|
+ # Prepend a brief preamble to the system prompt so the agent knows where
|
|
|
+ # its input data lives. This is purely informational — the agent still
|
|
|
+ # uses normal Bash/Read tools to access those files.
|
|
|
+ if source_dir and os.path.isdir(source_dir):
|
|
|
+ preamble = (
|
|
|
+ f"[数据来源目录]\n"
|
|
|
+ f"你的输入数据位于:{source_dir}\n"
|
|
|
+ f"请优先从该目录读取所需文件。\n\n"
|
|
|
+ )
|
|
|
+ existing_prompt = config.get("systemPrompt") or ""
|
|
|
+ config = dict(config) # shallow copy to avoid mutating caller's dict
|
|
|
+ config["systemPrompt"] = preamble + existing_prompt
|
|
|
+
|
|
|
+ # 创建 run workspace
|
|
|
+ # Priority: continue_workspace > run_dir > work_dir > agent_dir
|
|
|
workspace_path = ""
|
|
|
if continue_workspace and os.path.isdir(continue_workspace):
|
|
|
# Reuse the previous run's workspace dir. Don't call
|
|
|
@@ -1473,24 +1526,37 @@ def _execute_agent(config: dict, input_text: str, on_step=None,
|
|
|
except Exception as _e:
|
|
|
logger.warning("could not write continue marker: %s", _e)
|
|
|
logger.info("reusing workspace for continue-run: %s", workspace_path)
|
|
|
+ elif run_dir and os.path.isdir(run_dir):
|
|
|
+ # run_dir is the dedicated log/trace base; workspace lives DIRECTLY here
|
|
|
+ # (no extra "workspace/" subdir — the user already named this dir whatever
|
|
|
+ # they want, e.g. "…/review67/workspace"). bare=True skips the subdir.
|
|
|
+ workspace_path = create_run_workspace(run_dir, run_id, input_text, config, bare=True)
|
|
|
+ elif work_dir and os.path.isdir(work_dir):
|
|
|
+ # Fallback: create run workspace inside work_dir when run_dir is not set.
|
|
|
+ workspace_path = create_run_workspace(work_dir, run_id, input_text, config)
|
|
|
elif agent_dir:
|
|
|
workspace_path = create_run_workspace(agent_dir, run_id, input_text, config)
|
|
|
|
|
|
- # CRITICAL: align the Bash tool's session CWD with this run's workspace.
|
|
|
- # shell_tools._session_cwd is a *module-level* global captured at import
|
|
|
- # time (= the backend's startup CWD, typically the repo root). Without
|
|
|
- # this override every `{"tool":"Bash","input":{"command":"mkdir foo"}}`
|
|
|
- # creates files under the repo, not the run workspace. This is process-
|
|
|
- # global state — concurrent runs in the same process would race on it —
|
|
|
- # which is acceptable for the current single-tenant deployment but worth
|
|
|
- # revisiting (thread-local _session_cwd) before enabling parallel runs.
|
|
|
- if workspace_path:
|
|
|
+ # CRITICAL: align the Bash tool's session CWD.
|
|
|
+ # Shell starts in work_dir (where final outputs go) so the agent
|
|
|
+ # naturally reads/writes user files there. workspace_path is the
|
|
|
+ # log/trace dir — {workspace} variable in prompts points there —
|
|
|
+ # but Bash starts in work_dir.
|
|
|
+ # When work_dir is absent, fall back to workspace_path (existing behaviour).
|
|
|
+ _cwd_to_set = (
|
|
|
+ work_dir if (work_dir and os.path.isdir(work_dir)) else workspace_path
|
|
|
+ )
|
|
|
+ if _cwd_to_set:
|
|
|
try:
|
|
|
from lambdagent.builtin_tools.shell_tools import _set_cwd as _shell_set_cwd
|
|
|
- _shell_set_cwd(workspace_path)
|
|
|
- logger.info("aligned shell_tools session CWD to workspace: %s", workspace_path)
|
|
|
+ _shell_set_cwd(_cwd_to_set)
|
|
|
+ logger.info(
|
|
|
+ "shell CWD → %s (%s)",
|
|
|
+ _cwd_to_set,
|
|
|
+ "work_dir" if _cwd_to_set == work_dir else "workspace",
|
|
|
+ )
|
|
|
except Exception as _e:
|
|
|
- logger.warning("failed to align shell session CWD: %s", _e)
|
|
|
+ logger.warning("failed to set shell session CWD: %s", _e)
|
|
|
|
|
|
# 配置写入 workspace 或 /tmp
|
|
|
if workspace_path:
|