From b2745a6059c5d8aed154a6b27b540a4790a540ba Mon Sep 17 00:00:00 2001 From: Fennel1 <627235787@qq.com> Date: Sat, 18 Apr 2026 22:42:19 +0800 Subject: [PATCH 1/5] fix FE/BE domain --- .gitignore | 2 ++ .../app/models/distributed/sessions.py | 2 +- .../app/schemas/distributed/sessions.py | 2 +- apps/backend/app/services/llm.py | 8 +++++ .../api/v1/endpoints/test_distributed.py | 4 +-- apps/backend/tests/core/test_config.py | 2 +- .../tests/core/test_exception_handlers.py | 2 +- apps/backend/tests/models/test_models.py | 2 +- apps/backend/tests/schemas/test_contracts.py | 4 +-- apps/frontend/.env.example | 4 +-- apps/frontend/README.md | 2 +- apps/frontend/package.json | 2 +- apps/frontend/src/api/client.ts | 2 +- apps/frontend/src/api/runs.ts | 2 +- .../distributed/useDistributedController.ts | 2 +- .../frontend/src/features/simulation/utils.ts | 2 +- .../distributed/DistributedClientPanel.tsx | 2 +- config_schema.yaml | 2 +- configs/simulation_config.yaml | 2 +- docker-compose.dev.yml | 8 ++--- docker-compose.dist.yml | 36 +++++++++---------- scripts/docker-dev-build-proxy.sh | 4 +-- scripts/docker-dev-up.sh | 6 ++-- scripts/docker-dist-up.sh | 18 +++++----- 24 files changed, 66 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index c96ea54..3b88f71 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ configs/simulation_runs/ configs/agent_experiment_runs/ configs/agent_config_*.yaml configs/cached/ + +.pnpm-store/ \ No newline at end of file diff --git a/apps/backend/app/models/distributed/sessions.py b/apps/backend/app/models/distributed/sessions.py index 65fcd41..fd9bd1c 100644 --- a/apps/backend/app/models/distributed/sessions.py +++ b/apps/backend/app/models/distributed/sessions.py @@ -37,7 +37,7 @@ class DistributedSession(SQLModel, table=True): sa_column=Column(Integer, ForeignKey("distributed_jobs.id"), nullable=False, index=True), ) server_ip: str = Field( - default="127.0.0.1", + default="localhost", sa_column=Column(String(DistributedLimits.SERVER_IP_MAX_LENGTH), nullable=False), ) server_port: int = Field( diff --git a/apps/backend/app/schemas/distributed/sessions.py b/apps/backend/app/schemas/distributed/sessions.py index d658712..1d3f738 100644 --- a/apps/backend/app/schemas/distributed/sessions.py +++ b/apps/backend/app/schemas/distributed/sessions.py @@ -17,7 +17,7 @@ class DistributedSessionCreateRequest(BaseModel): Request payload for creating or getting distributed session. """ - server_ip: str = "127.0.0.1" + server_ip: str = "localhost" server_port: int = 50052 diff --git a/apps/backend/app/services/llm.py b/apps/backend/app/services/llm.py index 88e28db..00caa25 100644 --- a/apps/backend/app/services/llm.py +++ b/apps/backend/app/services/llm.py @@ -56,6 +56,14 @@ class LLMRegistry: "name": "gpt-4o", "llm": _build_openai_model("gpt-4o"), }, + { + "name": "bytedance-seed/seed-2.0-mini", + "llm": _build_openai_model("bytedance-seed/seed-2.0-mini"), + }, + { + "name": "moonshotai/kimi-k2.5", + "llm": _build_openai_model("moonshotai/kimi-k2.5"), + } ] @classmethod diff --git a/apps/backend/tests/api/v1/endpoints/test_distributed.py b/apps/backend/tests/api/v1/endpoints/test_distributed.py index 00cbf77..0a9ed4b 100644 --- a/apps/backend/tests/api/v1/endpoints/test_distributed.py +++ b/apps/backend/tests/api/v1/endpoints/test_distributed.py @@ -23,7 +23,7 @@ def test_distributed_session_lifecycle(client): created_session = client.post( f"/api/v1/distributed/jobs/{job_id}/session", - json={"server_ip": "127.0.0.1", "server_port": 50052}, + json={"server_ip": "localhost", "server_port": 50052}, ) assert created_session.status_code == 201 session_id = created_session.json()["id"] @@ -102,7 +102,7 @@ def test_distributed_start_requires_all_clients_ready(client): created_session = client.post( f"/api/v1/distributed/jobs/{job_id}/session", - json={"server_ip": "127.0.0.1", "server_port": 50052}, + json={"server_ip": "localhost", "server_port": 50052}, ) assert created_session.status_code == 201 session_id = created_session.json()["id"] diff --git a/apps/backend/tests/core/test_config.py b/apps/backend/tests/core/test_config.py index ca7dab9..b05f308 100644 --- a/apps/backend/tests/core/test_config.py +++ b/apps/backend/tests/core/test_config.py @@ -10,7 +10,7 @@ def _base_settings_kwargs() -> dict: "PROJECT_NAME": "Figaro", "API_PREFIX": "/api/v1", "FRONTEND_URL": "http://localhost:5173", - "ALLOWED_ORIGINS": "http://localhost:5173, http://127.0.0.1:5173", + "ALLOWED_ORIGINS": "http://localhost:5173, http://localhost:5173", "POSTGRES_HOST": "localhost", "POSTGRES_PORT": 5432, "POSTGRES_DB": "figaro", diff --git a/apps/backend/tests/core/test_exception_handlers.py b/apps/backend/tests/core/test_exception_handlers.py index 385975f..e339d87 100644 --- a/apps/backend/tests/core/test_exception_handlers.py +++ b/apps/backend/tests/core/test_exception_handlers.py @@ -19,7 +19,7 @@ def _build_request(path: str = "/test") -> Request: "raw_path": path.encode("utf-8"), "query_string": b"", "headers": [], - "client": ("127.0.0.1", 12345), + "client": ("localhost", 12345), "server": ("testserver", 80), } return Request(scope) diff --git a/apps/backend/tests/models/test_models.py b/apps/backend/tests/models/test_models.py index 9d08999..2a52ff8 100644 --- a/apps/backend/tests/models/test_models.py +++ b/apps/backend/tests/models/test_models.py @@ -46,7 +46,7 @@ def test_distributed_models_have_expected_defaults(): assert job.status == DistributedJobStatus.DRAFT assert job.expected_clients == 1 assert session.status == DistributedSessionStatus.WAITING_CLIENTS - assert session.server_ip == "127.0.0.1" + assert session.server_ip == "localhost" assert session.server_port == 50052 assert len(session.id) == 8 assert participant.status == DistributedParticipantStatus.PENDING_APPROVAL diff --git a/apps/backend/tests/schemas/test_contracts.py b/apps/backend/tests/schemas/test_contracts.py index ba6b545..488a136 100644 --- a/apps/backend/tests/schemas/test_contracts.py +++ b/apps/backend/tests/schemas/test_contracts.py @@ -33,7 +33,7 @@ def test_basic_request_schema_defaults(): sim_cfg = SimulationJobConfigUpdateRequest(config={"x": 1}) sim_copy = SimulationJobCopyRequest() - assert dist_session.server_ip == "127.0.0.1" + assert dist_session.server_ip == "localhost" assert dist_session.server_port == 50052 assert dist_cfg.config == {} assert message.detail is None @@ -64,7 +64,7 @@ def test_from_attributes_response_models_round_trip(): dist_session = DistributedSession( id="sess0001", job_id=2, - server_ip="127.0.0.1", + server_ip="localhost", server_port=50052, ) diff --git a/apps/frontend/.env.example b/apps/frontend/.env.example index 52caf89..1d9b979 100644 --- a/apps/frontend/.env.example +++ b/apps/frontend/.env.example @@ -1,2 +1,2 @@ -VITE_API_BASE_URL=http://127.0.0.1:8000 -OPENAPI_URL=http://127.0.0.1:8000/openapi.json +VITE_API_BASE_URL=http://localhost:8000 +OPENAPI_URL=http://localhost:8000/openapi.json diff --git a/apps/frontend/README.md b/apps/frontend/README.md index 2c21b42..25051f0 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -23,7 +23,7 @@ This updates `src/api/openapi.d.ts` from FastAPI OpenAPI schema. pnpm run dev ``` -Frontend uses `VITE_API_BASE_URL` (default: `http://127.0.0.1:8000`). +Frontend uses `VITE_API_BASE_URL` (default: `http://localhost:8000`). ## Pages/Features diff --git a/apps/frontend/package.json b/apps/frontend/package.json index d59a501..15847e6 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -8,7 +8,7 @@ "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", - "gen:api": "openapi-typescript ${OPENAPI_URL:-http://127.0.0.1:8000/openapi.json} -o src/api/openapi.d.ts" + "gen:api": "openapi-typescript ${OPENAPI_URL:-http://localhost:8000/openapi.json} -o src/api/openapi.d.ts" }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.14", diff --git a/apps/frontend/src/api/client.ts b/apps/frontend/src/api/client.ts index 2924daf..1c53624 100644 --- a/apps/frontend/src/api/client.ts +++ b/apps/frontend/src/api/client.ts @@ -1,7 +1,7 @@ import createClient from "openapi-fetch"; import type { paths } from "./openapi"; -export const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8000"; +export const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; export const api = createClient({ baseUrl diff --git a/apps/frontend/src/api/runs.ts b/apps/frontend/src/api/runs.ts index 50c30fc..799cddd 100644 --- a/apps/frontend/src/api/runs.ts +++ b/apps/frontend/src/api/runs.ts @@ -24,7 +24,7 @@ export type RunMetrics = { client_results: Record; }; -const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8000"; +const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; async function readJson(response: Response): Promise { if (!response.ok) { diff --git a/apps/frontend/src/features/distributed/useDistributedController.ts b/apps/frontend/src/features/distributed/useDistributedController.ts index 257d58a..455b9de 100644 --- a/apps/frontend/src/features/distributed/useDistributedController.ts +++ b/apps/frontend/src/features/distributed/useDistributedController.ts @@ -417,7 +417,7 @@ export function useDistributedController({ : {}; const configuredIp = String(getValueByPath(jobConfig, "distributed.server_ip") ?? "").trim(); const nextIp = - configuredIp.length > 0 && configuredIp !== "127.0.0.1" && configuredIp !== "localhost" + configuredIp.length > 0 && configuredIp !== "localhost" && configuredIp !== "localhost" ? configuredIp : DEFAULT_DISTRIBUTED_GRPC_HOST; const configuredPort = String(getValueByPath(jobConfig, "distributed.port") ?? "").trim(); diff --git a/apps/frontend/src/features/simulation/utils.ts b/apps/frontend/src/features/simulation/utils.ts index 66992e5..b6b9bd4 100644 --- a/apps/frontend/src/features/simulation/utils.ts +++ b/apps/frontend/src/features/simulation/utils.ts @@ -98,7 +98,7 @@ const CLIENT_SERIES_COLORS = [ ]; export const DEFAULT_DISTRIBUTED_SERVER_API_BASE = import.meta.env.VITE_DISTRIBUTED_SERVER_API_BASE ?? baseUrl; -export const DEFAULT_DISTRIBUTED_GRPC_HOST = import.meta.env.VITE_DISTRIBUTED_GRPC_HOST ?? "127.0.0.1"; +export const DEFAULT_DISTRIBUTED_GRPC_HOST = import.meta.env.VITE_DISTRIBUTED_GRPC_HOST ?? "localhost"; export const DEFAULT_DISTRIBUTED_GRPC_PORT = import.meta.env.VITE_DISTRIBUTED_GRPC_PORT ?? "50052"; export function nodeCenter(node: TopologyNode): Point { diff --git a/apps/frontend/src/pages/distributed/DistributedClientPanel.tsx b/apps/frontend/src/pages/distributed/DistributedClientPanel.tsx index 0071ab5..fc32f8e 100644 --- a/apps/frontend/src/pages/distributed/DistributedClientPanel.tsx +++ b/apps/frontend/src/pages/distributed/DistributedClientPanel.tsx @@ -39,7 +39,7 @@ export function DistributedClientPanel(props: DistributedPageProps) { setClientServerApiBase(event.target.value)} - placeholder="http://127.0.0.1:8000" + placeholder="http://localhost:8000" />
diff --git a/config_schema.yaml b/config_schema.yaml index 9f3a8da..b7fb373 100644 --- a/config_schema.yaml +++ b/config_schema.yaml @@ -110,7 +110,7 @@ distributed: role: "server" server_ip: type: "text" - default: "127.0.0.1" + default: "localhost" depends_on: "system.mode == distributed" port: type: "number" diff --git a/configs/simulation_config.yaml b/configs/simulation_config.yaml index b1c647d..073f5b0 100644 --- a/configs/simulation_config.yaml +++ b/configs/simulation_config.yaml @@ -31,7 +31,7 @@ system: mode: simulation node_role: server distributed: - server_ip: 127.0.0.1 + server_ip: localhost port: 50052 client_id: 0 logging: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 01c5f26..e5bb18e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -48,14 +48,14 @@ services: http_proxy: "" HTTPS_PROXY: "" https_proxy: "" - NO_PROXY: localhost,127.0.0.1,postgres - no_proxy: localhost,127.0.0.1,postgres + NO_PROXY: localhost,localhost,postgres + no_proxy: localhost,localhost,postgres NVIDIA_VISIBLE_DEVICES: all NVIDIA_DRIVER_CAPABILITIES: compute,utility PROJECT_NAME: Figaro API_PREFIX: /api/v1 FRONTEND_URL: http://localhost:5173 - ALLOWED_ORIGINS: http://localhost:5173,http://127.0.0.1:5173,http://localhost + ALLOWED_ORIGINS: http://localhost:5173,http://localhost:5173,http://localhost POSTGRES_HOST: postgres POSTGRES_PORT: 5432 POSTGRES_DB: ${POSTGRES_DB:-figaro} @@ -85,7 +85,7 @@ services: && pnpm install --frozen-lockfile && pnpm dev --host 0.0.0.0 --port 5173" environment: - VITE_API_BASE_URL: http://127.0.0.1:8000 + VITE_API_BASE_URL: http://localhost:8000 CHOKIDAR_USEPOLLING: "true" ports: - "5173:5173" diff --git a/docker-compose.dist.yml b/docker-compose.dist.yml index 83a8b42..9b27aba 100644 --- a/docker-compose.dist.yml +++ b/docker-compose.dist.yml @@ -28,8 +28,8 @@ x-backend-base: &backend-base http_proxy: "" HTTPS_PROXY: "" https_proxy: "" - NO_PROXY: localhost,127.0.0.1 - no_proxy: localhost,127.0.0.1 + NO_PROXY: localhost,localhost + no_proxy: localhost,localhost PROJECT_NAME: Figaro API_PREFIX: /api/v1 POSTGRES_PORT: 5432 @@ -56,7 +56,7 @@ x-frontend-base: &frontend-base && pnpm dev --host 0.0.0.0 --port 5173" environment: CHOKIDAR_USEPOLLING: "true" - VITE_DISTRIBUTED_SERVER_API_BASE: http://127.0.0.1:${DIST_SERVER_API_PORT:-8100} + VITE_DISTRIBUTED_SERVER_API_BASE: http://localhost:${DIST_SERVER_API_PORT:-8100} volumes: - ./:/workspace - figaro_dist_pnpm_store:/root/.local/share/pnpm/store @@ -99,8 +99,8 @@ services: PROJECT_NAME: Figaro API_PREFIX: /api/v1 FRONTEND_URL: http://localhost:${DIST_SERVER_WEB_PORT:-5273} - ALLOWED_ORIGINS: http://localhost:${DIST_SERVER_WEB_PORT:-5273},http://127.0.0.1:${DIST_SERVER_WEB_PORT:-5273} - POSTGRES_HOST: 127.0.0.1 + ALLOWED_ORIGINS: http://localhost:${DIST_SERVER_WEB_PORT:-5273},http://localhost:${DIST_SERVER_WEB_PORT:-5273} + POSTGRES_HOST: localhost POSTGRES_PORT: ${DIST_SERVER_POSTGRES_HOST_PORT:-55431} POSTGRES_DB: ${DIST_SERVER_POSTGRES_DB:-figaro_server} POSTGRES_USER: ${POSTGRES_USER:-postgres} @@ -120,9 +120,9 @@ services: container_name: figaro-dist-frontend-server environment: CHOKIDAR_USEPOLLING: "true" - VITE_API_BASE_URL: http://127.0.0.1:${DIST_SERVER_API_PORT:-8100} - VITE_DISTRIBUTED_SERVER_API_BASE: http://127.0.0.1:${DIST_SERVER_API_PORT:-8100} - VITE_DISTRIBUTED_GRPC_HOST: 127.0.0.1 + VITE_API_BASE_URL: http://localhost:${DIST_SERVER_API_PORT:-8100} + VITE_DISTRIBUTED_SERVER_API_BASE: http://localhost:${DIST_SERVER_API_PORT:-8100} + VITE_DISTRIBUTED_GRPC_HOST: localhost VITE_DISTRIBUTED_GRPC_PORT: "50052" ports: - "${DIST_SERVER_WEB_PORT:-5273}:5173" @@ -168,8 +168,8 @@ services: PROJECT_NAME: Figaro API_PREFIX: /api/v1 FRONTEND_URL: http://localhost:${DIST_CLIENT0_WEB_PORT:-5274} - ALLOWED_ORIGINS: http://localhost:${DIST_CLIENT0_WEB_PORT:-5274},http://127.0.0.1:${DIST_CLIENT0_WEB_PORT:-5274} - POSTGRES_HOST: 127.0.0.1 + ALLOWED_ORIGINS: http://localhost:${DIST_CLIENT0_WEB_PORT:-5274},http://localhost:${DIST_CLIENT0_WEB_PORT:-5274} + POSTGRES_HOST: localhost POSTGRES_PORT: ${DIST_CLIENT0_POSTGRES_HOST_PORT:-55432} POSTGRES_DB: ${DIST_CLIENT0_POSTGRES_DB:-figaro_client0} POSTGRES_USER: ${POSTGRES_USER:-postgres} @@ -189,9 +189,9 @@ services: container_name: figaro-dist-frontend-client0 environment: CHOKIDAR_USEPOLLING: "true" - VITE_API_BASE_URL: http://127.0.0.1:${DIST_CLIENT0_API_PORT:-8101} - VITE_DISTRIBUTED_SERVER_API_BASE: http://127.0.0.1:${DIST_SERVER_API_PORT:-8100} - VITE_DISTRIBUTED_GRPC_HOST: 127.0.0.1 + VITE_API_BASE_URL: http://localhost:${DIST_CLIENT0_API_PORT:-8101} + VITE_DISTRIBUTED_SERVER_API_BASE: http://localhost:${DIST_SERVER_API_PORT:-8100} + VITE_DISTRIBUTED_GRPC_HOST: localhost VITE_DISTRIBUTED_GRPC_PORT: "50052" ports: - "${DIST_CLIENT0_WEB_PORT:-5274}:5173" @@ -237,8 +237,8 @@ services: PROJECT_NAME: Figaro API_PREFIX: /api/v1 FRONTEND_URL: http://localhost:${DIST_CLIENT1_WEB_PORT:-5275} - ALLOWED_ORIGINS: http://localhost:${DIST_CLIENT1_WEB_PORT:-5275},http://127.0.0.1:${DIST_CLIENT1_WEB_PORT:-5275} - POSTGRES_HOST: 127.0.0.1 + ALLOWED_ORIGINS: http://localhost:${DIST_CLIENT1_WEB_PORT:-5275},http://localhost:${DIST_CLIENT1_WEB_PORT:-5275} + POSTGRES_HOST: localhost POSTGRES_PORT: ${DIST_CLIENT1_POSTGRES_HOST_PORT:-55433} POSTGRES_DB: ${DIST_CLIENT1_POSTGRES_DB:-figaro_client1} POSTGRES_USER: ${POSTGRES_USER:-postgres} @@ -258,9 +258,9 @@ services: container_name: figaro-dist-frontend-client1 environment: CHOKIDAR_USEPOLLING: "true" - VITE_API_BASE_URL: http://127.0.0.1:${DIST_CLIENT1_API_PORT:-8102} - VITE_DISTRIBUTED_SERVER_API_BASE: http://127.0.0.1:${DIST_SERVER_API_PORT:-8100} - VITE_DISTRIBUTED_GRPC_HOST: 127.0.0.1 + VITE_API_BASE_URL: http://localhost:${DIST_CLIENT1_API_PORT:-8102} + VITE_DISTRIBUTED_SERVER_API_BASE: http://localhost:${DIST_SERVER_API_PORT:-8100} + VITE_DISTRIBUTED_GRPC_HOST: localhost VITE_DISTRIBUTED_GRPC_PORT: "50052" ports: - "${DIST_CLIENT1_WEB_PORT:-5275}:5173" diff --git a/scripts/docker-dev-build-proxy.sh b/scripts/docker-dev-build-proxy.sh index 46d1133..f19600f 100755 --- a/scripts/docker-dev-build-proxy.sh +++ b/scripts/docker-dev-build-proxy.sh @@ -5,11 +5,11 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE_REL="docker-compose.dev.yml" SERVICE="${SERVICE:-backend}" -PROXY_HOSTPORT="${PROXY_HOSTPORT:-127.0.0.1:8888}" +PROXY_HOSTPORT="${PROXY_HOSTPORT:-localhost:8888}" HTTP_PROXY_VALUE="${HTTP_PROXY:-http://${PROXY_HOSTPORT}}" HTTPS_PROXY_VALUE="${HTTPS_PROXY:-${HTTP_PROXY_VALUE}}" ALL_PROXY_VALUE="${ALL_PROXY-${HTTP_PROXY_VALUE}}" -NO_PROXY_VALUE="${NO_PROXY:-localhost,127.0.0.1,postgres}" +NO_PROXY_VALUE="${NO_PROXY:-localhost,localhost,postgres}" USE_NO_CACHE=0 USE_PULL=0 diff --git a/scripts/docker-dev-up.sh b/scripts/docker-dev-up.sh index 7d80a33..75bccd3 100755 --- a/scripts/docker-dev-up.sh +++ b/scripts/docker-dev-up.sh @@ -18,9 +18,9 @@ echo "[figaro-dev] starting postgres + backend + frontend..." echo echo "[figaro-dev] started" -echo " Frontend: http://127.0.0.1:5173" -echo " Backend : http://127.0.0.1:8000" -echo " API Docs: http://127.0.0.1:8000/docs" +echo " Frontend: http://localhost:5173" +echo " Backend : http://localhost:8000" +echo " API Docs: http://localhost:8000/docs" echo echo "Logs:" echo " docker compose -f ${COMPOSE_FILE} logs -f --tail=200" diff --git a/scripts/docker-dist-up.sh b/scripts/docker-dist-up.sh index a7a2508..83b8ff3 100755 --- a/scripts/docker-dist-up.sh +++ b/scripts/docker-dist-up.sh @@ -44,17 +44,17 @@ echo echo "[figaro-dist] started" echo " Network mode: host (backend services)" echo " Server:" -echo " Frontend: http://127.0.0.1:${SERVER_WEB_PORT}" -echo " Backend : http://127.0.0.1:${SERVER_API_PORT}" -echo " OpenAPI : http://127.0.0.1:${SERVER_API_PORT}/docs" +echo " Frontend: http://localhost:${SERVER_WEB_PORT}" +echo " Backend : http://localhost:${SERVER_API_PORT}" +echo " OpenAPI : http://localhost:${SERVER_API_PORT}/docs" echo " Client0:" -echo " Frontend: http://127.0.0.1:${CLIENT0_WEB_PORT}" -echo " Backend : http://127.0.0.1:${CLIENT0_API_PORT}" -echo " OpenAPI : http://127.0.0.1:${CLIENT0_API_PORT}/docs" +echo " Frontend: http://localhost:${CLIENT0_WEB_PORT}" +echo " Backend : http://localhost:${CLIENT0_API_PORT}" +echo " OpenAPI : http://localhost:${CLIENT0_API_PORT}/docs" echo " Client1:" -echo " Frontend: http://127.0.0.1:${CLIENT1_WEB_PORT}" -echo " Backend : http://127.0.0.1:${CLIENT1_API_PORT}" -echo " OpenAPI : http://127.0.0.1:${CLIENT1_API_PORT}/docs" +echo " Frontend: http://localhost:${CLIENT1_WEB_PORT}" +echo " Backend : http://localhost:${CLIENT1_API_PORT}" +echo " OpenAPI : http://localhost:${CLIENT1_API_PORT}/docs" echo echo "Logs:" echo " docker compose -f ${COMPOSE_FILE} logs -f --tail=200" From fd579643fb89abc98aa68bedf8bf9cbcd79fd18c Mon Sep 17 00:00:00 2001 From: Fennel1 <627235787@qq.com> Date: Wed, 22 Apr 2026 00:55:50 +0800 Subject: [PATCH 2/5] update --- apps/backend/app/api/v1/endpoints/agent.py | 149 +++++++++- .../app/models/agent/optimization_jobs.py | 1 + apps/backend/app/schemas/agent.py | 33 +++ .../app/services/agent/experiment_service.py | 6 +- apps/backend/app/services/agent/graph.py | 52 +++- .../app/services/agent/history_service.py | 16 +- .../app/services/agent/runtime_service.py | 4 + apps/backend/app/services/agent/state.py | 2 + apps/backend/app/services/agent/summary.py | 20 ++ apps/frontend/package.json | 2 +- apps/frontend/src/App.tsx | 92 +++--- apps/frontend/src/api/agent.ts | 61 +++- .../components/AgentExperimentStudio.tsx | 73 +++++ .../agent/components/AgentPlanPreview.tsx | 196 ++++++++++++ .../agent/components/AgentResultsCompare.tsx | 207 +++++++++++++ .../agent/components/AgentRunDashboard.tsx | 265 +++++++++++++++++ .../src/features/agent/useAgentController.ts | 93 +++++- .../components/MiniScatterChart.tsx | 81 +++++ apps/frontend/src/pages/AgentPage.tsx | 278 +----------------- apps/frontend/src/pages/types.ts | 15 + 20 files changed, 1324 insertions(+), 322 deletions(-) create mode 100644 apps/frontend/src/features/agent/components/AgentExperimentStudio.tsx create mode 100644 apps/frontend/src/features/agent/components/AgentPlanPreview.tsx create mode 100644 apps/frontend/src/features/agent/components/AgentResultsCompare.tsx create mode 100644 apps/frontend/src/features/agent/components/AgentRunDashboard.tsx create mode 100644 apps/frontend/src/features/simulation/components/MiniScatterChart.tsx diff --git a/apps/backend/app/api/v1/endpoints/agent.py b/apps/backend/app/api/v1/endpoints/agent.py index 2265a3f..63c5888 100644 --- a/apps/backend/app/api/v1/endpoints/agent.py +++ b/apps/backend/app/api/v1/endpoints/agent.py @@ -4,7 +4,8 @@ from __future__ import annotations -from fastapi import APIRouter, status +from fastapi import APIRouter, status, HTTPException +import json from app.api.deps import AsyncSessionDep from app.schemas.agent import ( @@ -27,6 +28,8 @@ from app.services.agent.graph import FederatedAgentGraphBuilder from app.services.agent.objectives import AgentOptimizationObjective, resolve_objective from app.services.llm import LLMRegistry, LLMService +from app.schemas.agent import AgentPlanPreviewRequest, AgentPlanPreviewResponse, ExperimentPlanPreview, AgentPlanReviseRequest +import uuid agent_router = APIRouter() agent_runtime_service = AgentRuntimeService() @@ -143,6 +146,7 @@ def _build_progress_response(snapshot: dict) -> AgentOptimizeProgressResponse: best_config=snapshot.get("best_config"), best_metrics=best_metrics, experiments=_build_experiments(snapshot.get("experiments", [])), + draft_experiments=snapshot.get("draft_experiments", []), summary_text=snapshot.get("summary_text"), error_message=snapshot.get("error_message"), created_at=snapshot.get("created_at"), @@ -243,6 +247,7 @@ async def start_optimization( system_mode=payload.system_mode, model_name=payload.model_name, objective=payload.objective, + planned_experiments=payload.planned_experiments, ) return _build_progress_response(snapshot) @@ -463,3 +468,145 @@ async def get_agent_run_logs( ) for log in logs ] + + +@agent_router.post( + "/optimize/plan", + response_model=AgentPlanPreviewResponse, + status_code=status.HTTP_200_OK, + summary="Generate a plan preview and save as draft", +) +async def generate_plan_preview( + payload: AgentPlanPreviewRequest, + session: AsyncSessionDep, +) -> AgentPlanPreviewResponse: + """ + 1. Call LLM to generate the plan. + 2. Create a Job record with PENDING_REVIEW status. + """ + # Borrow the graph builder to do the LLM parsing + llm_service = LLMService() + builder = FederatedAgentGraphBuilder(llm_service=llm_service, session=session) + + # Construct a dummy initial state to run through the parse node + temp_state = AgentState( + goal=payload.goal, + job_name=payload.job_name, + model_name=payload.model_name, + system_mode=payload.system_mode, + max_iterations=10, + objective=AgentOptimizationObjective.AUTO, + resolved_objective=AgentOptimizationObjective.ACCURACY, + ) + + # Call the graph's parse node directly to get the LLM result + parsed_state = await builder._node_parse(temp_state) + + previews = [] + for exp in parsed_state.experiments: + previews.append(ExperimentPlanPreview( + name=exp.name, + plan_summary=exp.plan_summary or "", + config_patch=exp.config_patch, + # Fallback values if LLM doesn't generate them + estimated_minutes=0, + estimated_gpu_vram_gb=0.0, + )) + + # Persist the draft (write snapshot to DB, set status to pending_review) + history_service = AgentOptimizationHistoryService(session) + + job = await history_service.create_job( + task_id=f"draft-{uuid.uuid4().hex[:8]}", + goal=payload.goal, + job_name=payload.job_name, + model_name=payload.model_name, + system_mode=payload.system_mode, + max_iterations=len(previews) or 1, + status="pending_review", + snapshot={ + "goal": payload.goal, + "job_name": payload.job_name, + "status": "pending_review", + "experiments": [], + "draft_experiments": [p.model_dump() for p in previews] + } + ) + + return AgentPlanPreviewResponse( + optimization_job_id=job.id, + goal=payload.goal, + experiments=previews, + system_mode=payload.system_mode + ) + +@agent_router.post( + "/optimization-jobs/{job_id}/revise", + response_model=AgentPlanPreviewResponse, + status_code=status.HTTP_200_OK, + summary="Revise a pending plan draft using natural language", +) +async def revise_plan_preview( + job_id: int, + payload: AgentPlanReviseRequest, + session: AsyncSessionDep, +) -> AgentPlanPreviewResponse: + history_service = AgentOptimizationHistoryService(session) + job = await history_service.get_job_or_raise(job_id) + + current_status = job.status.value if hasattr(job.status, "value") else str(job.status) + if current_status != "pending_review": + raise HTTPException(status_code=400, detail="Only pending_review jobs can be revised.") + + snapshot = job.snapshot_json + current_experiments = snapshot.get("draft_experiments", []) + if not current_experiments: + raise HTTPException(status_code=400, detail="No draft experiments found to revise.") + + instructions = ( + "You are an AI assistant managing a Federated Learning experiment plan. " + "The user wants to modify the current experiment configuration based on their feedback. " + "Update the JSON configuration to reflect their request. " + "Respond ONLY with a valid JSON array of experiment objects, matching the original schema. " + "Do not include any other text." + ) + + input_text = ( + f"Current Plan (JSON):\n{json.dumps(current_experiments, indent=2)}\n\n" + f"User Modification Request:\n{payload.instruction}\n\n" + "Please output the updated JSON array of experiments:" + ) + + llm_service = LLMService() + model_name = snapshot.get("model_name") + text, _ = await llm_service.generate_text( + model=model_name if model_name else None, + instructions=instructions, + input_text=input_text, + ) + + clean_text = text.strip() + if clean_text.startswith("```json"): + clean_text = clean_text[7:] + elif clean_text.startswith("```"): + clean_text = clean_text[3:] + if clean_text.endswith("```"): + clean_text = clean_text[:-3] + clean_text = clean_text.strip() + + try: + updated_experiments = json.loads(clean_text) + if not isinstance(updated_experiments, list): + raise ValueError("LLM did not return a list.") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to parse LLM response: {str(e)}") + + snapshot["draft_experiments"] = updated_experiments + await history_service.update_job_snapshot(task_id=job.task_id, snapshot=snapshot) + + return AgentPlanPreviewResponse( + optimization_job_id=job.id, + goal=snapshot.get("goal", ""), + experiments=[ExperimentPlanPreview.model_validate(exp) for exp in updated_experiments], + system_mode=snapshot.get("system_mode", "simulation") + ) \ No newline at end of file diff --git a/apps/backend/app/models/agent/optimization_jobs.py b/apps/backend/app/models/agent/optimization_jobs.py index bde431c..8848516 100644 --- a/apps/backend/app/models/agent/optimization_jobs.py +++ b/apps/backend/app/models/agent/optimization_jobs.py @@ -21,6 +21,7 @@ class AgentOptimizationJobStatus(str, Enum): RUNNING = "running" COMPLETED = "completed" FAILED = "failed" + PENDING_REVIEW = "pending_review" class AgentOptimizationJob(SQLModel, table=True): diff --git a/apps/backend/app/schemas/agent.py b/apps/backend/app/schemas/agent.py index 89ea8e4..f11614f 100644 --- a/apps/backend/app/schemas/agent.py +++ b/apps/backend/app/schemas/agent.py @@ -41,6 +41,10 @@ class AgentOptimizeRequest(BaseModel): default=AgentOptimizationObjective.AUTO, description="Objective for the experiment batch. 'accuracy' maximizes accuracy; 'auto' defaults to accuracy.", ) + planned_experiments: Optional[list[dict[str, Any]]] = Field( + default=None, + description="Explicit list of experiment patches to run, bypassing LLM parse." + ) class AgentModelsResponse(BaseModel): @@ -143,6 +147,7 @@ class AgentOptimizeProgressResponse(BaseModel): best_config: dict[str, Any] | None = None best_metrics: SimulationRunMetricsResponse | None = None experiments: list[AgentExperimentSummary] = Field(default_factory=list) + draft_experiments: list[dict[str, Any]] = Field(default_factory=list) summary_text: str | None = None error_message: str | None = None created_at: datetime | None = None @@ -215,3 +220,31 @@ class AgentRunMetricsResponse(BaseModel): run_id: str metrics: dict[str, Any] = Field(default_factory=dict) + + +class ExperimentPlanPreview(BaseModel): + """Single experiment draft for frontend UI display and editing.""" + name: str + plan_summary: str + config_patch: dict[str, Any] + estimated_minutes: int = Field(default=0, description="Estimated time in minutes") + estimated_gpu_vram_gb: float = Field(default=0.0, description="Estimated VRAM in GB") + risk_warnings: list[str] = Field(default_factory=list) + +class AgentPlanPreviewRequest(BaseModel): + """Payload for requesting a draft generation without execution.""" + goal: str = Field(..., description="Natural-language experiment request.") + job_name: str = Field(..., description="Name for the job group.") + model_name: Optional[str] = None + system_mode: str = "simulation" + +class AgentPlanPreviewResponse(BaseModel): + """Draft results returned to the frontend.""" + optimization_job_id: int + goal: str + experiments: list[ExperimentPlanPreview] + system_mode: str + +class AgentPlanReviseRequest(BaseModel): + """Payload for requesting a revision to an existing plan draft.""" + instruction: str = Field(..., description="Natural language feedback to revise the plan.") \ No newline at end of file diff --git a/apps/backend/app/services/agent/experiment_service.py b/apps/backend/app/services/agent/experiment_service.py index f37afca..601e932 100644 --- a/apps/backend/app/services/agent/experiment_service.py +++ b/apps/backend/app/services/agent/experiment_service.py @@ -33,6 +33,10 @@ ) from app.repositories.agent import AgentExperimentRepository +from app.core.logger import get_logger + +logger = get_logger(__name__) + # --------------------------------------------------------------------------- # Shared constants (mirrored from simulation run_service / run_metrics_service) @@ -472,11 +476,9 @@ async def get_run_metrics(self, run_id: str) -> dict[str, Any]: run = await self.repo.get_run(run_id) if not run: raise exceptions.RunNotFound("Run not found") - # If terminal and already has metrics, return them if run.status in TERMINAL_RUN_STATUSES and isinstance(run.metrics_json, dict) and run.metrics_json: return self._normalize_metrics_payload(run.metrics_json) - # Try live result file live_result_path = self._results_dir() / self._build_live_results_filename(run_id) loaded = self._load_metrics_file(live_result_path) diff --git a/apps/backend/app/services/agent/graph.py b/apps/backend/app/services/agent/graph.py index 3e6fcd6..cdaabd8 100644 --- a/apps/backend/app/services/agent/graph.py +++ b/apps/backend/app/services/agent/graph.py @@ -13,6 +13,9 @@ import uuid from collections.abc import Awaitable, Callable +from app.core.logger import get_logger +logger = get_logger(__name__) + from langgraph.graph import END, StateGraph # type: ignore[import-untyped] from sqlalchemy.ext.asyncio import AsyncSession @@ -71,6 +74,25 @@ async def _node_parse(self, state: AgentState) -> AgentState: state.phase = "parsing" await self._publish_progress(state) + if getattr(state, "planned_experiments", None): + experiments: list[ExperimentPlan] = [] + for idx, exp in enumerate(state.planned_experiments): + name = exp.get("name", f"exp-{idx + 1}") + experiments.append( + ExperimentPlan( + iteration=idx + 1, + iteration_goal=f"Run experiment: {name}", + name=name, + plan_summary=exp.get("plan_summary", ""), + config_patch=exp.get("config_patch", {}), + ) + ) + state.experiments = experiments + state.max_iterations = len(experiments) + state.phase = "parsed" + await self._publish_progress(state) + return state + experiment_service = AgentExperimentService(self._session) schema = experiment_service.get_config_schema() capabilities = get_platform_capabilities() @@ -83,16 +105,30 @@ async def _node_parse(self, state: AgentState) -> AgentState: base_config=base_config, ) + logger.info("llm input instructions=%s", instructions) + logger.info("llm input prompt=%s", prompt) + text, _ = await self._llm.generate_text( model=select_llm_model(state.model_name), instructions=instructions, input_text=prompt, ) + logger.info("llm raw output=%s", text) + + clean_text = text.strip() + if clean_text.startswith("```json"): + clean_text = clean_text[7:] + elif clean_text.startswith("```"): + clean_text = clean_text[3:] + if clean_text.endswith("```"): + clean_text = clean_text[:-3] + clean_text = clean_text.strip() + # Parse LLM response into experiment plans experiments: list[ExperimentPlan] = [] try: - parsed = json.loads(text) + parsed = json.loads(clean_text) if isinstance(parsed, dict): plan_summary = parsed.get("plan_summary", "") raw_experiments = parsed.get("experiments", []) @@ -101,14 +137,16 @@ async def _node_parse(self, state: AgentState) -> AgentState: if not isinstance(exp, dict): continue name = exp.get("name", f"exp-{idx + 1}") - config = exp.get("config", {}) - if not isinstance(config, dict): - config = {} + patch_config = exp.get("config", {}) + if not isinstance(patch_config, dict): + patch_config = {} # Merge with base config and normalize - merged = self._deep_merge_dicts(base_config, config) + current_full_config = copy.deepcopy(base_config) + merged = self._deep_merge_dicts(current_full_config, patch_config) try: normalized = experiment_service.normalize_simulation_config(merged) - except exceptions.BadRequestError: + except Exception: + # 如果合并出错,至少保证能跑,回退到基础配置 normalized = experiment_service.normalize_simulation_config( copy.deepcopy(base_config) ) @@ -118,7 +156,7 @@ async def _node_parse(self, state: AgentState) -> AgentState: iteration_goal=f"Run experiment: {name}", name=name, plan_summary=plan_summary, - config_patch=config, + config_patch=normalized, ) ) except (json.JSONDecodeError, Exception): diff --git a/apps/backend/app/services/agent/history_service.py b/apps/backend/app/services/agent/history_service.py index 637132a..7dce611 100644 --- a/apps/backend/app/services/agent/history_service.py +++ b/apps/backend/app/services/agent/history_service.py @@ -37,9 +37,15 @@ async def create_job( model_name: str | None, max_iterations: int, snapshot: dict[str, Any], + status: AgentOptimizationJobStatus | str = AgentOptimizationJobStatus.QUEUED, ) -> AgentOptimizationJob: if job_name: await self._ensure_unique_job_name(job_name) + + resolved_status = status + if isinstance(status, str): + resolved_status = AgentOptimizationJobStatus(status.lower()) + try: job = await self.repository.create_job( task_id=task_id, @@ -47,7 +53,7 @@ async def create_job( goal=goal, system_mode=system_mode, model_name=model_name, - status=AgentOptimizationJobStatus.QUEUED, + status=resolved_status, max_iterations=max_iterations, snapshot_json=self._to_json_safe(snapshot), ) @@ -120,8 +126,12 @@ async def mark_job_failed( async def _ensure_unique_job_name(self, job_name: str, *, exclude_job_id: int | None = None) -> None: existing = await self.repository.get_job_by_name(job_name) if existing and (exclude_job_id is None or existing.id != exclude_job_id): - raise exceptions.JobAlreadyExists(self.JOB_NAME_CONFLICT_MESSAGE) - + current_status = existing.status.value if hasattr(existing.status, "value") else str(existing.status) + if current_status.lower() == "pending_review": + await self.session.delete(existing) + await self.session.flush() + else: + raise exceptions.JobAlreadyExists(self.JOB_NAME_CONFLICT_MESSAGE) @staticmethod def _extract_simulation_job_id(snapshot: dict[str, Any]) -> int | None: current_experiment = snapshot.get("current_experiment") diff --git a/apps/backend/app/services/agent/runtime_service.py b/apps/backend/app/services/agent/runtime_service.py index 79661f3..5a3335b 100644 --- a/apps/backend/app/services/agent/runtime_service.py +++ b/apps/backend/app/services/agent/runtime_service.py @@ -39,6 +39,7 @@ async def start_optimization( model_name: str | None, job_name: str | None, objective: AgentOptimizationObjective, + planned_experiments: list[dict[str, Any]], ) -> dict[str, Any]: """ Create a background experiment task and return its initial snapshot. @@ -98,6 +99,7 @@ async def start_optimization( job_name=job_name, objective=objective, resolved_objective=resolved_objective, + planned_experiments=planned_experiments, ) ) task.add_done_callback(lambda finished_task, current_task_id=task_id: self._on_task_done(current_task_id, finished_task)) @@ -124,6 +126,7 @@ async def _run_task( job_name: str | None, objective: AgentOptimizationObjective, resolved_objective: AgentOptimizationObjective, + planned_experiments: list[dict[str, Any]] | None = None, ) -> None: """Execute one experiment task in the background.""" logger.info("agent_task_started task_id=%s", task_id) @@ -138,6 +141,7 @@ async def _run_task( objective=objective, resolved_objective=resolved_objective, phase="parsing", + planned_experiments=planned_experiments, ) await self._update_from_state(task_id, initial_state, status="running") async with AsyncSessionLocal() as session: diff --git a/apps/backend/app/services/agent/state.py b/apps/backend/app/services/agent/state.py index e34ba4d..911274b 100644 --- a/apps/backend/app/services/agent/state.py +++ b/apps/backend/app/services/agent/state.py @@ -102,3 +102,5 @@ class AgentState: history: list[ExperimentRecord] = field(default_factory=list) terminated: bool = False summary: str | None = None + + planned_experiments: list[dict[str, Any]] | None = None diff --git a/apps/backend/app/services/agent/summary.py b/apps/backend/app/services/agent/summary.py index 2fac63e..04e0b5c 100644 --- a/apps/backend/app/services/agent/summary.py +++ b/apps/backend/app/services/agent/summary.py @@ -137,3 +137,23 @@ def build_lessons_learned( ) -> list[str]: """Bench mode does not generate iterative lessons.""" return [] + +def calculate_communication_cost(config: dict[str, Any]) -> int: + """Estimate communication cost: total rounds * clients per round.""" + federated = config.get("federated") or {} + rounds = int(federated.get("num_rounds", 0)) + clients = int(federated.get("clients_per_round", 0)) + return rounds * clients + +def calculate_computation_cost(config: dict[str, Any]) -> int: + """Estimate computation cost: total rounds * clients per round * local epochs.""" + federated = config.get("federated") or {} + epochs = int(federated.get("local_epochs", 0)) + return calculate_communication_cost(config) * epochs + +def get_best_experiment(state: AgentState) -> ExperimentRecord | None: + """Find the best performing experiment (the Winner).""" + scored = [r for r in state.experiment_results if r.score is not None] + if not scored: + return None + return max(scored, key=lambda r: r.score) \ No newline at end of file diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 15847e6..c5d3584 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -9,7 +9,7 @@ "build": "tsc -b && vite build", "preview": "vite preview", "gen:api": "openapi-typescript ${OPENAPI_URL:-http://localhost:8000/openapi.json} -o src/api/openapi.d.ts" - }, + }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.16", diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index caeaace..33ccb15 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -1,58 +1,74 @@ -import { useState } from "react"; -import { BriefcaseBusiness, Loader2 } from "lucide-react"; - +import { BriefcaseBusiness, Sparkles, LayoutDashboard, LineChart } from "lucide-react"; import { Card, CardContent } from "./components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "./components/ui/tabs"; + import { useAgentController } from "./features/agent/useAgentController"; -import { useDistributedController } from "./features/distributed/useDistributedController"; -import { useSimulationController } from "./features/simulation/useSimulationController"; -import { AgentPage } from "./pages/AgentPage"; -import { DistributedPage } from "./pages/DistributedPage"; -import { SimulationPage } from "./pages/SimulationPage"; -import type { PageMode } from "./pages/types"; +import { AgentExperimentStudio } from "./features/agent/components/AgentExperimentStudio"; +import { AgentPlanPreview } from "./features/agent/components/AgentPlanPreview"; +import { AgentRunDashboard } from "./features/agent/components/AgentRunDashboard"; +import { AgentResultsCompare } from "./features/agent/components/AgentResultsCompare"; export default function App() { - const [pageMode, setPageMode] = useState("agent"); - const simulationPageProps = useSimulationController(); - const agentPageProps = useAgentController(); - const distributedPageProps = useDistributedController({ - pageMode, - configSchema: simulationPageProps.configSchema, - }); - const busy = simulationPageProps.busy || distributedPageProps.busy || agentPageProps.busy; + const agentProps = useAgentController(); + const { workflowStep, setWorkflowStep, draftPlan } = agentProps; + + const activeTab = + (workflowStep === "home" || workflowStep === "preview") ? "studio" : + (workflowStep === "running") ? "dashboard" : + "results"; + + const handleTabChange = (val: string) => { + if (val === "studio") { + setWorkflowStep(draftPlan ? "preview" : "home"); + } else if (val === "dashboard") { + setWorkflowStep("running"); + } else if (val === "results") { + setWorkflowStep("results"); + } + }; return ( -
-
- +
+
+ + {/* 顶层全局导航栏 */} +
- -

Figaro Control Center

+
+ +
+

+ Figaro Agent Studio +

+
- setPageMode(value as PageMode)}> - - Agent - Simulation - Distributed + + + + Experiment Studio + + + Run Dashboard + + + Results & History +
- {pageMode === "simulation" && } - {pageMode === "distributed" && } - {pageMode === "agent" && } - - {busy && ( -
- - Processing... -
- )} + {/* 动态内容渲染区 */} +
+ {activeTab === "studio" && workflowStep === "home" && } + {activeTab === "studio" && workflowStep === "preview" && } + {activeTab === "dashboard" && } + {activeTab === "results" && } +
); -} +} \ No newline at end of file diff --git a/apps/frontend/src/api/agent.ts b/apps/frontend/src/api/agent.ts index d5e61ac..3a67e27 100644 --- a/apps/frontend/src/api/agent.ts +++ b/apps/frontend/src/api/agent.ts @@ -90,6 +90,7 @@ export type AgentOptimizeProgressResponse = { best_config: Record | null; best_metrics: RunMetrics | null; experiments: AgentExperimentSummary[]; + draft_experiments?: any[]; summary_text: string | null; error_message: string | null; created_at: string | null; @@ -153,6 +154,33 @@ export type AgentRunLogResponse = { created_at: string; }; +export interface ExperimentPlanPreview { + name: string; + plan_summary: string; + config_patch: Record; + estimated_minutes?: number; + estimated_gpu_vram_gb?: number; + risk_warnings?: string[]; +}; + +export interface AgentPlanPreviewRequest { + goal: string; + job_name: string; + model_name: string | null; + system_mode: string; +}; + +export interface AgentPlanPreviewResponse { + optimization_job_id: number; + goal: string; + experiments: ExperimentPlanPreview[]; + system_mode: string; +}; + +export interface AgentPlanReviseRequest { + instruction: string; +}; + async function readJson(response: Response): Promise { if (!response.ok) { throw new Error(await getHttpErrorMessage(response)); @@ -235,4 +263,35 @@ export const agentApi = { const response = await fetch(`${baseUrl}/api/v1/agent/runs/${encodeURIComponent(runId)}/logs`); return readJson(response); }, -}; + + async generatePlan(request: AgentPlanPreviewRequest): Promise { + const response = await fetch(`${baseUrl}/api/v1/agent/optimize/plan`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + return readJson(response); + }, + + async startOptimizeFromDraft(request: any): Promise { + const response = await fetch(`${baseUrl}/api/v1/agent/optimize/start`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + return readJson(response); + }, + + async revisePlan(jobId: number, request: AgentPlanReviseRequest): Promise { + const response = await fetch(`${baseUrl}/api/v1/agent/optimization-jobs/${encodeURIComponent(jobId)}/revise`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + if (!response.ok) { + const err = await response.json().catch(() => ({})); + throw new Error(err.detail || "Failed to revise plan"); + } + return response.json(); + }, +}; \ No newline at end of file diff --git a/apps/frontend/src/features/agent/components/AgentExperimentStudio.tsx b/apps/frontend/src/features/agent/components/AgentExperimentStudio.tsx new file mode 100644 index 0000000..f48e902 --- /dev/null +++ b/apps/frontend/src/features/agent/components/AgentExperimentStudio.tsx @@ -0,0 +1,73 @@ +import { Sparkles, ArrowRight, Loader2 } from "lucide-react"; +import { Button } from "../../../components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../../components/ui/card"; +import { Textarea } from "../../../components/ui/textarea"; +import { Input } from "../../../components/ui/input"; +import type { AgentPageProps } from "../../../pages/types"; + +export function AgentExperimentStudio(props: AgentPageProps) { + const { goal, setGoal, jobName, setJobName, presets, busy, handleGeneratePlan } = props; + + return ( +
+
+

Figaro Experiment Studio

+

Describe your federated learning goals. The agent will design the configuration.

+
+ + + +
+ + setJobName(e.target.value)} + placeholder="e.g., resnet-cifar-tuning" + /> +
+ +
+ +