Compare commits

..

4 Commits

Author SHA1 Message Date
737a80aa39 llm name fix 2026-03-18 20:44:28 +08:00
bdd4ddec9e show tool message in chat 2026-03-14 11:20:39 +08:00
2ee55d25cc bug fix: skill_dir show up correctly in frontend 2026-03-13 16:09:26 +08:00
bf9ce709e2 bug fix; error when saving config for fs_backend 2026-03-13 16:01:45 +08:00
5 changed files with 221 additions and 27 deletions

View File

@@ -46,7 +46,7 @@ type EditableAgent = {
type AgentChatMessage = {
id: string;
role: "user" | "assistant";
role: "user" | "assistant" | "tool";
content: string;
};
@@ -452,6 +452,28 @@ function createConversationId(): string {
return `conv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function mapConversationMessageToAgentChatMessage(
message: ConversationMessageItem
): AgentChatMessage | null {
const type = (message.message_type || "").toLowerCase();
let role: AgentChatMessage["role"] | null = null;
if (type === "human" || type === "user") {
role = "user";
} else if (type === "ai" || type === "assistant") {
role = "assistant";
} else if (type === "tool") {
role = "tool";
}
if (!role) {
return null;
}
return {
id: `${message.sequence_number}-${message.created_at}-${role}`,
role,
content: message.content || "",
};
}
function normalizeDeepAgentActBackend(value: unknown): DeepAgentActBackend {
if (value === "local_shell" || value === "localshell") {
return "local_shell";
@@ -465,6 +487,62 @@ function normalizeDeepAgentActBackend(value: unknown): DeepAgentActBackend {
return DEFAULT_DEEPAGENT_ACT_BACKEND;
}
function getDefaultFileBackendConfig(
backend: DeepAgentActBackend
): FileBackendConfig {
return { ...DEFAULT_FILE_BACKEND_CONFIG[backend] };
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readOptionalString(
record: Record<string, unknown>,
key: keyof FileBackendConfig
): string | undefined {
const value = record[key];
return typeof value === "string" ? value : undefined;
}
function getDeepAgentEditorState(config: GraphConfigReadResponse): {
actBackend: DeepAgentActBackend;
fileBackendConfig: FileBackendConfig;
} {
const graphParams = isRecord(config.graph_params) ? config.graph_params : {};
const actBackend = normalizeDeepAgentActBackend(graphParams.act_bkend);
const defaults = getDefaultFileBackendConfig(actBackend);
const rawFileBackendConfig = isRecord(graphParams.file_backend_config)
? graphParams.file_backend_config
: null;
if (!rawFileBackendConfig) {
return {
actBackend,
fileBackendConfig: defaults,
};
}
return {
actBackend,
fileBackendConfig: {
...defaults,
...(readOptionalString(rawFileBackendConfig, "skills_dir") !== undefined
? { skills_dir: readOptionalString(rawFileBackendConfig, "skills_dir") as string }
: {}),
...(readOptionalString(rawFileBackendConfig, "rt_skills_dir") !== undefined
? { rt_skills_dir: readOptionalString(rawFileBackendConfig, "rt_skills_dir") as string }
: {}),
...(readOptionalString(rawFileBackendConfig, "workspace_dir") !== undefined
? { workspace_dir: readOptionalString(rawFileBackendConfig, "workspace_dir") as string }
: {}),
...(readOptionalString(rawFileBackendConfig, "api_key") !== undefined
? { api_key: readOptionalString(rawFileBackendConfig, "api_key") as string }
: {}),
},
};
}
function buildGraphParams(editor: EditableAgent): Record<string, unknown> {
if (editor.graphId === "deepagent") {
return {
@@ -479,6 +557,7 @@ function toEditable(
config: GraphConfigReadResponse,
draft: boolean
): EditableAgent {
const deepAgentState = getDeepAgentEditorState(config);
return {
id: draft
? `draft-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
@@ -492,8 +571,8 @@ function toEditable(
prompts: config.prompt_dict || {},
apiKey: config.api_key || DEFAULT_API_KEY,
llmName: DEFAULT_LLM_NAME,
actBackend: DEFAULT_DEEPAGENT_ACT_BACKEND,
fileBackendConfig: DEFAULT_FILE_BACKEND_CONFIG[DEFAULT_DEEPAGENT_ACT_BACKEND],
actBackend: deepAgentState.actBackend,
fileBackendConfig: deepAgentState.fileBackendConfig,
};
}
@@ -788,8 +867,8 @@ export default function App() {
: DEFAULT_DEEPAGENT_ACT_BACKEND,
fileBackendConfig:
graphId === "deepagent"
? prev.fileBackendConfig || DEFAULT_FILE_BACKEND_CONFIG[DEFAULT_DEEPAGENT_ACT_BACKEND]
: DEFAULT_FILE_BACKEND_CONFIG[DEFAULT_DEEPAGENT_ACT_BACKEND],
? prev.fileBackendConfig || getDefaultFileBackendConfig(DEFAULT_DEEPAGENT_ACT_BACKEND)
: getDefaultFileBackendConfig(DEFAULT_DEEPAGENT_ACT_BACKEND),
};
if (next.isDraft) {
setDraftAgents((drafts) => drafts.map((draft) => (draft.id === next.id ? next : draft)));
@@ -827,7 +906,7 @@ export default function App() {
setEditorAndSyncDraft((prev) => ({
...prev,
actBackend: newBackend,
fileBackendConfig: DEFAULT_FILE_BACKEND_CONFIG[newBackend],
fileBackendConfig: getDefaultFileBackendConfig(newBackend),
}));
}
@@ -1170,6 +1249,7 @@ export default function App() {
async function sendAgentChatMessage(): Promise<void> {
const pipelineId = (chatPipelineId || "").trim();
const conversationId = chatConversationId;
const message = chatInput.trim();
if (!pipelineId || !message || chatSending) {
return;
@@ -1199,7 +1279,7 @@ export default function App() {
try {
await streamAgentChatResponse({
appId: pipelineId,
sessionId: chatConversationId,
sessionId: conversationId,
apiKey: authKey,
message,
signal: controller.signal,
@@ -1216,6 +1296,34 @@ export default function App() {
);
},
});
// Some runtimes namespace thread ids as "<pipeline_id>:<session_id>" when persisting.
// Try both IDs and fail soft so a successful streamed reply never turns into a UI error.
const candidateConversationIds = [
conversationId,
`${pipelineId}:${conversationId}`,
];
let reloaded = false;
for (const candidateId of candidateConversationIds) {
try {
const storedMessages = await getPipelineConversationMessages(
pipelineId,
candidateId
);
const normalizedMessages = storedMessages
.map(mapConversationMessageToAgentChatMessage)
.filter((item): item is AgentChatMessage => item !== null);
if (normalizedMessages.length > 0) {
setChatMessages(normalizedMessages);
reloaded = true;
break;
}
} catch {
// Ignore refresh failures; keep streamed content visible.
}
}
if (!reloaded) {
// Keep existing streamed messages without surfacing a false error state.
}
} catch (error) {
if ((error as Error).message === "Request cancelled") {
setChatMessages((prev) =>
@@ -1850,9 +1958,15 @@ export default function App() {
chatMessages.map((message) => (
<article
key={message.id}
className={`chat-modal-message ${message.role === "assistant" ? "assistant" : "user"}`}
className={`chat-modal-message ${message.role}`}
>
<strong>{message.role === "assistant" ? "Agent" : "You"}</strong>
<strong>
{message.role === "assistant"
? "Agent"
: message.role === "tool"
? "Tool"
: "You"}
</strong>
<div className="chat-message-content">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{message.content || (chatSending && message.role === "assistant" ? "..." : "")}

View File

@@ -611,6 +611,10 @@ button:disabled {
border-left: 3px solid #26a269;
}
.chat-modal-message.tool {
border-left: 3px solid #8e6bd8;
}
.chat-modal-message p {
margin: 6px 0 0 0;
white-space: pre-wrap;

View File

@@ -23,6 +23,7 @@ export type GraphConfigReadResponse = {
tool_keys: string[];
prompt_dict: Record<string, string>;
api_key: string;
graph_params?: Record<string, unknown>;
};
export type GraphConfigUpsertRequest = {

View File

@@ -20,6 +20,7 @@ from lang_agent.config.constants import (
MCP_CONFIG_DEFAULT_CONTENT,
PIPELINE_REGISTRY_PATH,
)
from lang_agent.config.core_config import load_tyro_conf
from lang_agent.front_api.build_server_utils import (
GRAPH_BUILD_FNCS,
update_pipeline_registry,
@@ -55,6 +56,7 @@ class GraphConfigReadResponse(BaseModel):
tool_keys: List[str]
prompt_dict: Dict[str, str]
api_key: str = Field(default="")
graph_params: Dict[str, Any] = Field(default_factory=dict)
class GraphConfigListItem(BaseModel):
@@ -325,6 +327,81 @@ def _normalize_pipeline_spec(pipeline_id: str, spec: Dict[str, Any]) -> Pipeline
)
def _resolve_config_path(config_file: str) -> str:
if osp.isabs(config_file):
return config_file
return osp.join(_PROJECT_ROOT, config_file)
def _normalize_deepagent_backend_name(file_backend_config: Any) -> Optional[str]:
if file_backend_config is None:
return None
type_names = {
type(file_backend_config).__name__.lower(),
getattr(getattr(file_backend_config, "_target", None), "__name__", "").lower(),
}
if any("statebk" in name for name in type_names):
return "state_bk"
if any("localshell" in name for name in type_names):
return "local_shell"
if any("daytona" in name for name in type_names):
return "daytona_sandbox"
return None
def _extract_graph_params_from_config(graph_id: Optional[str], loaded_cfg: Any) -> Dict[str, Any]:
if graph_id != "deepagent":
return {}
graph_config = getattr(loaded_cfg, "graph_config", None)
file_backend_config = getattr(graph_config, "file_backend_config", None)
if file_backend_config is None:
return {}
graph_params: Dict[str, Any] = {}
act_bkend = _normalize_deepagent_backend_name(file_backend_config)
if act_bkend:
graph_params["act_bkend"] = act_bkend
serialized_backend_config: Dict[str, Any] = {}
for key in ("skills_dir", "rt_skills_dir", "workspace_dir", "api_key"):
value = getattr(file_backend_config, key, None)
if value is not None:
serialized_backend_config[key] = value
if serialized_backend_config:
graph_params["file_backend_config"] = serialized_backend_config
return graph_params
def _load_graph_params_for_pipeline(
pipeline_id: str, graph_id: Optional[str]
) -> Dict[str, Any]:
try:
registry = _read_pipeline_registry()
pipeline_spec = registry.get("pipelines", {}).get(pipeline_id, {})
config_file = ""
if isinstance(pipeline_spec, dict):
config_file = str(pipeline_spec.get("config_file") or "").strip()
if not config_file:
fallback = osp.join(_PROJECT_ROOT, "configs", "pipelines", f"{pipeline_id}.yaml")
if osp.exists(fallback):
config_file = fallback
if not config_file:
return {}
config_path = _resolve_config_path(config_file)
if not osp.exists(config_path):
return {}
loaded_cfg = load_tyro_conf(config_path)
return _extract_graph_params_from_config(graph_id, loaded_cfg)
except Exception:
return {}
def _normalize_api_key_policy(api_key: str, policy: Dict[str, Any]) -> ApiKeyPolicyItem:
if not isinstance(policy, dict):
raise ValueError(f"api key policy for '{api_key}' must be an object")
@@ -428,6 +505,9 @@ async def get_default_graph_config(pipeline_id: str):
tool_keys=tool_keys,
prompt_dict=prompt_dict,
api_key=(active.get("api_key") or ""),
graph_params=_load_graph_params_for_pipeline(
pipeline_id, active.get("graph_id")
),
)
@@ -466,6 +546,9 @@ async def get_graph_config(pipeline_id: str, prompt_set_id: str):
tool_keys=tool_keys,
prompt_dict=prompt_dict,
api_key=(meta.get("api_key") or ""),
graph_params=_load_graph_params_for_pipeline(
pipeline_id, meta.get("graph_id")
),
)

View File

@@ -77,6 +77,8 @@ def build_route(
cmd_opt = [
"--pipeline.pipeline-id",
pipeline_id,
"--pipeline.llm-name",
llm_name,
"route", # ------------
"--llm-name",
llm_name,
@@ -125,6 +127,8 @@ def build_react(
cmd_opt = [
"--pipeline.pipeline-id",
pipeline_id,
"--pipeline.llm-name",
llm_name,
"react", # ------------
"--llm-name",
llm_name,
@@ -171,6 +175,8 @@ def build_deep_agent(
cmd_opt = [
"--pipeline.pipeline-id",
pipeline_id,
"--pipeline.llm-name",
llm_name,
"deepagent",
"--llm-name",
llm_name,
@@ -192,33 +198,19 @@ def build_deep_agent(
if file_backend_config:
if "skills_dir" in file_backend_config and file_backend_config["skills_dir"]:
cmd_opt.extend(
["--file-backend-config.skills-dir", file_backend_config["skills_dir"]]
)
cmd_opt.extend(["--skills-dir", file_backend_config["skills_dir"]])
if (
"rt_skills_dir" in file_backend_config
and file_backend_config["rt_skills_dir"]
):
cmd_opt.extend(
[
"--file-backend-config.rt-skills-dir",
file_backend_config["rt_skills_dir"],
]
)
cmd_opt.extend(["--rt-skills-dir", file_backend_config["rt_skills_dir"]])
if (
"workspace_dir" in file_backend_config
and file_backend_config["workspace_dir"]
):
cmd_opt.extend(
[
"--file-backend-config.workspace-dir",
file_backend_config["workspace_dir"],
]
)
cmd_opt.extend(["--workspace-dir", file_backend_config["workspace_dir"]])
if "api_key" in file_backend_config and file_backend_config["api_key"]:
cmd_opt.extend(
["--file-backend-config.api-key", file_backend_config["api_key"]]
)
cmd_opt.extend(["--api-key", file_backend_config["api_key"]])
return _build_and_load_pipeline_config(pipeline_id, pipeline_config_dir, cmd_opt)