diff --git a/service/app/api/v1/marketplace.py b/service/app/api/v1/marketplace.py index 03e59505..35881926 100644 --- a/service/app/api/v1/marketplace.py +++ b/service/app/api/v1/marketplace.py @@ -11,7 +11,7 @@ - GET /my-listings: Get current user's published listings """ -from typing import Literal +from typing import Any, Literal from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query @@ -76,6 +76,7 @@ class UpdateAgentRequest(BaseModel): tags: list[str] | None = None readme: str | None = None commit_message: str + graph_config: dict[str, Any] | None = None class ForkRequest(BaseModel): @@ -633,7 +634,7 @@ async def update_agent_and_listing( try: updated_listing = await marketplace_service.update_agent_and_publish( - marketplace_id, update_data, request.commit_message + marketplace_id, update_data, request.commit_message, request.graph_config ) if not updated_listing: raise HTTPException(status_code=404, detail="Marketplace listing not found") diff --git a/service/app/core/marketplace/agent_marketplace_service.py b/service/app/core/marketplace/agent_marketplace_service.py index 77806b96..941094b6 100644 --- a/service/app/core/marketplace/agent_marketplace_service.py +++ b/service/app/core/marketplace/agent_marketplace_service.py @@ -57,9 +57,10 @@ async def create_snapshot_from_agent(self, agent: Agent, commit_message: str) -> "tags": agent.tags or [], "model": agent.model, "temperature": agent.temperature, - "prompt": agent.prompt, + "prompt": agent.prompt, # Legacy field, kept for backward compat "require_tool_confirmation": agent.require_tool_confirmation, "scope": agent.scope, + "graph_config": agent.graph_config, # Source of truth for agent configuration } # Serialize MCP server metadata (no credentials) @@ -252,11 +253,12 @@ async def fork_agent(self, marketplace_id: UUID, user_id: str, fork_name: str | tags=config.get("tags", []), model=config.get("model"), temperature=config.get("temperature"), - prompt=config.get("prompt"), + prompt=config.get("prompt"), # Legacy field for backward compat require_tool_confirmation=config.get("require_tool_confirmation", False), provider_id=None, # User must configure their own provider knowledge_set_id=None, # Create empty knowledge set mcp_server_ids=[], # Will link compatible MCPs below + graph_config=config.get("graph_config"), # Restore from snapshot ) # Create the forked agent @@ -518,6 +520,7 @@ async def update_agent_and_publish( marketplace_id: UUID, agent_update: AgentMarketplaceUpdate, commit_message: str, + graph_config: dict[str, Any] | None = None, ) -> AgentMarketplace | None: """ Updates the underlying agent and publishes a new version. @@ -526,6 +529,7 @@ async def update_agent_and_publish( marketplace_id: The marketplace listing ID. agent_update: Data to update (name, description, tags, readme). commit_message: Description of the update. + graph_config: Optional graph configuration to update. Returns: The updated marketplace listing. @@ -547,6 +551,7 @@ async def update_agent_and_publish( description=agent_update.description, avatar=agent_update.avatar, tags=agent_update.tags, + graph_config=graph_config, # Update graph_config if provided ) await self.agent_repo.update_agent(agent.id, update_data) # Refresh agent to get latest state for snapshot @@ -633,8 +638,9 @@ async def pull_listing_update(self, agent_id: UUID, user_id: str) -> Agent: tags=config.get("tags", []), model=config.get("model"), temperature=config.get("temperature"), - prompt=config.get("prompt"), + prompt=config.get("prompt"), # Legacy field for backward compat require_tool_confirmation=config.get("require_tool_confirmation", False), + graph_config=config.get("graph_config"), # Sync from marketplace ) updated_agent = await self.agent_repo.update_agent(agent.id, update_data) diff --git a/service/app/core/prompts/defaults.py b/service/app/core/prompts/defaults.py index 22b97bb2..804eee63 100644 --- a/service/app/core/prompts/defaults.py +++ b/service/app/core/prompts/defaults.py @@ -73,8 +73,34 @@ def merge_prompt_configs(base: PromptConfig, override: PromptConfig) -> PromptCo return PromptConfig.model_validate(base_dict) +def get_display_prompt_from_config(config: dict[str, Any]) -> str | None: + """ + Extract display prompt from snapshot configuration for UI purposes. + + Priority: + 1. graph_config.prompt_config.custom_instructions + 2. Legacy prompt field + + Args: + config: The snapshot configuration dict + + Returns: + The display prompt string or None if not found + """ + # Priority 1: Check graph_config.prompt_config.custom_instructions + graph_config = config.get("graph_config") + if graph_config: + prompt_config = graph_config.get("prompt_config", {}) + if custom_instructions := prompt_config.get("custom_instructions"): + return custom_instructions + + # Priority 2: Legacy prompt field + return config.get("prompt") + + __all__ = [ "DEFAULT_PROMPT_CONFIG", "get_prompt_config_from_graph_config", "merge_prompt_configs", + "get_display_prompt_from_config", ] diff --git a/service/app/schemas/model_tier.py b/service/app/schemas/model_tier.py index 16a750a5..0d2296bd 100644 --- a/service/app/schemas/model_tier.py +++ b/service/app/schemas/model_tier.py @@ -57,7 +57,8 @@ class TierModelCandidate: TierModelCandidate( model="Vendor2/Claude-4.5-Opus", provider_type=ProviderType.GPUGEEK, - priority=1, + is_fallback=True, + priority=99, capabilities=["reasoning", "creative", "coding"], description="Best for coding and choose this for most tasks. Exceptional in complex coding, agentic tasks, and reasoning; highly reliable for software engineering.", ), @@ -104,7 +105,8 @@ class TierModelCandidate: TierModelCandidate( model="qwen3-max", provider_type=ProviderType.QWEN, - priority=2, + is_fallback=True, + priority=99, capabilities=["coding", "multilingual"], description="Choose this if user uses Chinese. Impressive in programming, agent tasks, and multilingual support with high benchmark scores; strong for coding and math.", ), diff --git a/web/src/app/marketplace/AgentMarketplaceDetail.tsx b/web/src/app/marketplace/AgentMarketplaceDetail.tsx index 08f83baa..47a1b05d 100644 --- a/web/src/app/marketplace/AgentMarketplaceDetail.tsx +++ b/web/src/app/marketplace/AgentMarketplaceDetail.tsx @@ -22,6 +22,35 @@ import { import { HeartIcon as HeartSolidIcon } from "@heroicons/react/24/solid"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { AgentSnapshot } from "@/service/marketplaceService"; + +// Helper to detect agent type from graph_config +function getAgentType( + graphConfig: Record | null | undefined, +): string { + if (!graphConfig) return "ReAct"; + const metadata = graphConfig.metadata as Record | undefined; + if (metadata?.builtin_key) return String(metadata.builtin_key); + if (metadata?.pattern) return String(metadata.pattern); + // Check if it has custom nodes beyond the standard react pattern + const nodes = graphConfig.nodes as Array | undefined; + if (nodes && nodes.length > 2) return "Custom Graph"; + return "ReAct"; +} + +// Helper to extract display prompt from configuration +function getDisplayPrompt( + config: AgentSnapshot["configuration"], +): string | null { + // Check graph_config.prompt_config.custom_instructions first + const gc = config.graph_config as Record | undefined; + if (gc?.prompt_config) { + const pc = gc.prompt_config as Record; + if (pc.custom_instructions) return String(pc.custom_instructions); + } + // Fallback to legacy prompt + return config.prompt || null; +} interface AgentMarketplaceDetailProps { marketplaceId: string; @@ -303,10 +332,15 @@ export default function AgentMarketplaceDetail({
{listing.snapshot ? ( <> -
+
v{listing.snapshot.version} + + {getAgentType( + listing.snapshot.configuration.graph_config, + )} + {listing.snapshot.commit_message} @@ -325,14 +359,16 @@ export default function AgentMarketplaceDetail({ )} {/* System Prompt */} - {listing.snapshot.configuration.prompt && ( + {getDisplayPrompt(listing.snapshot.configuration) && (

{t("marketplace.detail.config.systemPrompt")}

-                                {listing.snapshot.configuration.prompt}
+                                {getDisplayPrompt(
+                                  listing.snapshot.configuration,
+                                )}
                               
diff --git a/web/src/app/marketplace/AgentMarketplaceManage.tsx b/web/src/app/marketplace/AgentMarketplaceManage.tsx index c939355d..d8bf0ada 100644 --- a/web/src/app/marketplace/AgentMarketplaceManage.tsx +++ b/web/src/app/marketplace/AgentMarketplaceManage.tsx @@ -2,6 +2,8 @@ import { PlateReadmeEditor } from "@/components/editor/PlateReadmeEditor"; import { PlateReadmeViewer } from "@/components/editor/PlateReadmeViewer"; +import { AgentGraphEditor } from "@/components/editors/AgentGraphEditor"; +import { JsonEditor } from "@/components/editors/JsonEditor"; import ConfirmationModal from "@/components/modals/ConfirmationModal"; import { useListingHistory, @@ -11,11 +13,15 @@ import { } from "@/hooks/useMarketplace"; import type { AgentSnapshot } from "@/service/marketplaceService"; import { marketplaceService } from "@/service/marketplaceService"; +import type { GraphConfig } from "@/types/graphConfig"; import { ArrowLeftIcon, ArrowPathIcon, CheckCircleIcon, ClockIcon, + CodeBracketIcon, + Cog6ToothIcon, + CubeTransparentIcon, DocumentTextIcon, EyeIcon, GlobeAltIcon, @@ -23,8 +29,11 @@ import { PencilIcon, TrashIcon, } from "@heroicons/react/24/outline"; +import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react"; import { useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; interface AgentMarketplaceManageProps { marketplaceId: string; @@ -41,12 +50,23 @@ export default function AgentMarketplaceManage({ marketplaceId, onBack, }: AgentMarketplaceManageProps) { + const { t } = useTranslation(); const [showUnpublishConfirm, setShowUnpublishConfirm] = useState(false); const [isEditingReadme, setIsEditingReadme] = useState(false); const [readmeContent, setReadmeContent] = useState(""); const [isSavingReadme, setIsSavingReadme] = useState(false); const [isPublishing, setIsPublishing] = useState(false); - const [activeTab, setActiveTab] = useState<"editor" | "history">("editor"); + const [activeTab, setActiveTab] = useState<"editor" | "config" | "history">( + "editor", + ); + + // Configuration editing state + const [isEditingConfig, setIsEditingConfig] = useState(false); + const [graphConfig, setGraphConfig] = useState(null); + const [graphConfigJson, setGraphConfigJson] = useState(""); + const [graphConfigError, setGraphConfigError] = useState(null); + const [activeEditorTab, setActiveEditorTab] = useState(0); + const [isSavingConfig, setIsSavingConfig] = useState(false); const queryClient = useQueryClient(); @@ -157,6 +177,164 @@ export default function AgentMarketplaceManage({ ); }; + // Configuration editing handlers + const handleGraphConfigChange = useCallback((config: GraphConfig) => { + setGraphConfig(config); + setGraphConfigJson(JSON.stringify(config, null, 2)); + setGraphConfigError(null); + }, []); + + const handleJsonChange = useCallback( + (value: string) => { + setGraphConfigJson(value); + if (!value.trim()) { + setGraphConfig(null); + setGraphConfigError(null); + return; + } + try { + const parsed = JSON.parse(value) as GraphConfig; + setGraphConfig(parsed); + setGraphConfigError(null); + } catch { + setGraphConfigError(t("marketplace.manage.config.invalidJson")); + } + }, + [t], + ); + + const handleJsonValidation = useCallback( + (isValid: boolean, errors: string[]) => { + setGraphConfigError( + isValid + ? null + : errors[0] || t("marketplace.manage.config.invalidJson"), + ); + }, + [t], + ); + + const handleEditorTabChange = useCallback( + (index: number) => { + if (activeEditorTab === 1 && index === 0 && graphConfigJson.trim()) { + try { + const parsed = JSON.parse(graphConfigJson) as GraphConfig; + setGraphConfig(parsed); + setGraphConfigError(null); + } catch { + // Keep error + } + } + setActiveEditorTab(index); + }, + [activeEditorTab, graphConfigJson], + ); + + const startEditingConfig = () => { + const config = listing?.snapshot?.configuration?.graph_config; + if (config) { + const parsed = config as unknown as GraphConfig; + setGraphConfig(parsed); + setGraphConfigJson(JSON.stringify(config, null, 2)); + } else { + // Create a default ReAct config if none exists + // Must match backend's REACT_CONFIG structure for validation + const defaultConfig: GraphConfig = { + version: "2.0", + metadata: { + builtin_key: "react", + pattern: "react", + display_name: "ReAct Agent", + }, + nodes: [ + { + id: "agent", + name: "ReAct Agent", + type: "llm", + llm_config: { + prompt_template: "You are a helpful assistant.", + tools_enabled: true, + output_key: "response", + }, + }, + { + id: "tools", + name: "Tool Executor", + type: "tool", + tool_config: { + execute_all: true, + }, + }, + ], + edges: [ + { from_node: "START", to_node: "agent" }, + { from_node: "agent", to_node: "tools", condition: "has_tool_calls" }, + { from_node: "agent", to_node: "END", condition: "no_tool_calls" }, + { from_node: "tools", to_node: "agent" }, + ], + entry_point: "agent", + }; + setGraphConfig(defaultConfig); + setGraphConfigJson(JSON.stringify(defaultConfig, null, 2)); + } + setGraphConfigError(null); + setActiveEditorTab(0); + setIsEditingConfig(true); + }; + + const cancelEditingConfig = () => { + setIsEditingConfig(false); + setGraphConfig(null); + setGraphConfigJson(""); + setGraphConfigError(null); + }; + + const saveConfig = async () => { + if (!listing) return; + if (graphConfigError) { + return; + } + + let finalGraphConfig: Record | null = null; + if (graphConfigJson.trim()) { + try { + finalGraphConfig = JSON.parse(graphConfigJson); + } catch { + setGraphConfigError(t("marketplace.manage.config.invalidJson")); + return; + } + } + + try { + setIsSavingConfig(true); + // Use the updateAgentAndPublish endpoint to update the agent and create a new version + await marketplaceService.updateAgentAndPublish(listing.id, { + commit_message: t("marketplace.manage.config.commitMessage", { + defaultValue: "Updated agent configuration", + }), + graph_config: finalGraphConfig, + }); + + // Invalidate queries to refresh data + queryClient.invalidateQueries({ + queryKey: ["marketplace", "listing", listing.id], + }); + queryClient.invalidateQueries({ + queryKey: ["marketplace", "history", listing.id], + }); + + setIsEditingConfig(false); + setGraphConfig(null); + setGraphConfigJson(""); + toast.success(t("marketplace.manage.config.success")); + } catch (error) { + console.error("Failed to update configuration:", error); + toast.error(t("marketplace.manage.config.error")); + } finally { + setIsSavingConfig(false); + } + }; + // Loading state if (isLoading) { return ( @@ -253,6 +431,17 @@ export default function AgentMarketplaceManage({ Editor +
{/* Tab Content */} - {activeTab === "editor" ? ( + {activeTab === "editor" && ( /* README Editor */
@@ -339,7 +528,148 @@ export default function AgentMarketplaceManage({ )}
- ) : ( + )} + + {activeTab === "config" && ( + /* Configuration Editor */ +
+
+
+ +

+ {t("marketplace.manage.config.title")} +

+
+ {!isEditingConfig && ( + + )} +
+ +
+ {isEditingConfig ? ( +
+

+ {t("marketplace.manage.config.description")} +

+ + {/* Graph/JSON Editor Tabs */} + + + + + {t("marketplace.manage.config.visualEditor")} + + + + {t("marketplace.manage.config.jsonEditor")} + {graphConfigError && ( + + )} + + + + + {/* Visual Editor Panel */} + + + + + {/* JSON Editor Panel */} + +

+ {t("marketplace.manage.config.jsonDescription")} +

+
+ +
+ {graphConfigError && ( +

+ {graphConfigError} +

+ )} +
+
+
+ + {/* Actions */} +
+ + +
+
+ ) : ( +
+ {listing.snapshot?.configuration?.graph_config ? ( +
+ {}} + readOnly={true} + height="100%" + /> +
+ ) : ( +
+ +

{t("marketplace.manage.config.empty")}

+ +
+ )} +
+ )} +
+
+ )} + + {activeTab === "history" && ( /* Version History */
{isLoadingHistory ? ( diff --git a/web/src/i18n/locales/en/marketplace.json b/web/src/i18n/locales/en/marketplace.json index 69dc2204..1e1da8b1 100644 --- a/web/src/i18n/locales/en/marketplace.json +++ b/web/src/i18n/locales/en/marketplace.json @@ -161,5 +161,33 @@ "saveAsDraft": "Save as Draft", "publishing": "Publishing..." } + }, + "simplePromptEditor": { + "label": "System Prompt", + "description": "Define how your agent should behave and respond to users.", + "placeholder": "Enter instructions for your agent...", + "complexAgent": { + "title": "Complex Agent Configuration", + "description": "This agent uses a custom graph configuration. Use the full Workflow Editor to modify its behavior." + } + }, + "manage": { + "config": { + "title": "Configuration", + "description": "Edit your agent's graph configuration and workflow.", + "save": "Save & Publish", + "saving": "Saving...", + "cancel": "Cancel", + "edit": "Edit Configuration", + "commitMessage": "Updated agent configuration", + "success": "Configuration updated and published successfully!", + "error": "Failed to update configuration. Please try again.", + "visualEditor": "Visual Editor", + "jsonEditor": "JSON Editor", + "jsonDescription": "Edit the raw JSON configuration. Changes will be synced with the visual editor.", + "invalidJson": "Invalid JSON format", + "empty": "No configuration available.", + "configureNow": "Configure now" + } } } diff --git a/web/src/i18n/locales/ja/marketplace.json b/web/src/i18n/locales/ja/marketplace.json index aa4e5f3a..9fe9dcc6 100644 --- a/web/src/i18n/locales/ja/marketplace.json +++ b/web/src/i18n/locales/ja/marketplace.json @@ -161,5 +161,33 @@ "saveAsDraft": "下書きとして保存", "publishing": "公開中..." } + }, + "simplePromptEditor": { + "label": "システムプロンプト", + "description": "エージェントがどのように動作し、ユーザーに応答するかを定義します。", + "placeholder": "エージェントへの指示を入力...", + "complexAgent": { + "title": "複雑なエージェント設定", + "description": "このエージェントはカスタムグラフ設定を使用しています。動作を変更するには、完全なワークフローエディタを使用してください。" + } + }, + "manage": { + "config": { + "title": "設定", + "description": "エージェントのグラフ設定とワークフローを編集します。", + "save": "保存して公開", + "saving": "保存中...", + "cancel": "キャンセル", + "edit": "設定を編集", + "commitMessage": "エージェント設定を更新", + "success": "設定が更新・公開されました!", + "error": "設定の更新に失敗しました。もう一度お試しください。", + "visualEditor": "ビジュアルエディタ", + "jsonEditor": "JSON エディタ", + "jsonDescription": "生のJSON設定を編集します。変更はビジュアルエディタと同期されます。", + "invalidJson": "無効なJSON形式", + "empty": "設定情報はありません。", + "configureNow": "今すぐ設定" + } } } diff --git a/web/src/i18n/locales/zh/marketplace.json b/web/src/i18n/locales/zh/marketplace.json index f70afd4c..49d7572d 100644 --- a/web/src/i18n/locales/zh/marketplace.json +++ b/web/src/i18n/locales/zh/marketplace.json @@ -161,5 +161,33 @@ "saveAsDraft": "保存为草稿", "publishing": "发布中..." } + }, + "simplePromptEditor": { + "label": "系统提示词", + "description": "定义你的助手如何行为和响应用户。", + "placeholder": "输入助手的指令...", + "complexAgent": { + "title": "复杂助手配置", + "description": "此助手使用自定义图形配置。请使用完整的工作流编辑器来修改其行为。" + } + }, + "manage": { + "config": { + "title": "配置", + "description": "编辑助手的图形配置和工作流。", + "save": "保存并发布", + "saving": "保存中...", + "cancel": "取消", + "edit": "编辑配置", + "commitMessage": "更新助手配置", + "success": "配置已更新并发布成功!", + "error": "更新配置失败,请重试。", + "visualEditor": "可视化编辑器", + "jsonEditor": "JSON 编辑器", + "jsonDescription": "编辑原始 JSON 配置。更改将与可视化编辑器同步。", + "invalidJson": "无效的 JSON 格式", + "empty": "暂无配置信息。", + "configureNow": "立即配置" + } } } diff --git a/web/src/service/marketplaceService.ts b/web/src/service/marketplaceService.ts index 08df7ec3..c58ed64c 100644 --- a/web/src/service/marketplaceService.ts +++ b/web/src/service/marketplaceService.ts @@ -38,9 +38,10 @@ export interface AgentSnapshot { tags: string[]; model?: string; temperature?: number; - prompt?: string; + prompt?: string; // Legacy field, kept for backward compat require_tool_confirmation: boolean; scope: string; + graph_config?: Record | null; // Source of truth for agent configuration }; mcp_server_configs: Array<{ id: string; @@ -70,6 +71,7 @@ export interface UpdateAgentRequest { tags?: string[]; readme?: string | null; commit_message: string; + graph_config?: Record | null; } export interface PublishRequest { @@ -117,6 +119,7 @@ export interface RequirementsResponse { file_count: number; } | null; provider_needed: boolean; + graph_config?: Record | null; // For agent type detection } export interface SearchParams {