diff --git a/apps/web/src/components/agent-dashboard/AgentSystemPromptsCard.tsx b/apps/web/src/components/agent-dashboard/AgentSystemPromptsCard.tsx index 6c3be692..db40fa29 100644 --- a/apps/web/src/components/agent-dashboard/AgentSystemPromptsCard.tsx +++ b/apps/web/src/components/agent-dashboard/AgentSystemPromptsCard.tsx @@ -1,15 +1,16 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { AgentSystemPrompts, PromptKind, updateAgentPrompt } from '@/lib/agent-dashboard'; +import { useAgentPrompts } from '@/hooks/useAgentPrompts'; interface AgentSystemPromptsCardProps { agentId: string; - systemPrompts?: AgentSystemPrompts; + systemPrompts?: AgentSystemPrompts; // Legacy/Initial data canEdit?: boolean; onUpdated?: () => void; } @@ -43,17 +44,56 @@ const PROMPT_KINDS: { id: PromptKind; label: string; icon: string; description: export function AgentSystemPromptsCard({ agentId, - systemPrompts, + systemPrompts: initialPrompts, canEdit = false, onUpdated }: AgentSystemPromptsCardProps) { + const { prompts: fetchedPromptsList, mutate } = useAgentPrompts(agentId); + + // Transform list to dict structure + const systemPrompts = useMemo(() => { + if (!fetchedPromptsList || fetchedPromptsList.length === 0) { + return initialPrompts || {}; + } + + const dict: AgentSystemPrompts = { + core: null, + safety: null, + governance: null, + tools: null + }; + + fetchedPromptsList.forEach(p => { + if (p.kind in dict) { + dict[p.kind] = { + content: p.content, + version: p.version, + updated_at: p.updated_at || new Date().toISOString(), + updated_by: 'system' // Not returned by all endpoints yet + }; + } + }); + + return dict; + }, [fetchedPromptsList, initialPrompts]); + const [activeTab, setActiveTab] = useState('core'); const [editedContent, setEditedContent] = useState>({ - core: systemPrompts?.core?.content || '', - safety: systemPrompts?.safety?.content || '', - governance: systemPrompts?.governance?.content || '', - tools: systemPrompts?.tools?.content || '' + core: '', + safety: '', + governance: '', + tools: '' }); + + // Sync edited content when active tab or prompts change + useEffect(() => { + const current = systemPrompts?.[activeTab]; + setEditedContent(prev => ({ + ...prev, + [activeTab]: current?.content || '' + })); + }, [activeTab, systemPrompts]); + const [saving, setSaving] = useState(false); const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [error, setError] = useState(null); @@ -71,6 +111,7 @@ export function AgentSystemPromptsCard({ try { await updateAgentPrompt(agentId, activeTab, currentContent); + await mutate(); // Refresh data setSaveStatus('success'); onUpdated?.(); diff --git a/apps/web/src/hooks/useAgentPrompts.ts b/apps/web/src/hooks/useAgentPrompts.ts new file mode 100644 index 00000000..8e4bf29c --- /dev/null +++ b/apps/web/src/hooks/useAgentPrompts.ts @@ -0,0 +1,38 @@ +import useSWR from 'swr'; + +export type PromptKind = 'core' | 'safety' | 'governance' | 'tools'; + +export interface AgentPrompt { + id?: string; + kind: PromptKind; + content: string; + version: number; + updated_at?: string; + note?: string; +} + +export interface AgentPromptList { + agent_id: string; + prompts: AgentPrompt[]; +} + +const fetcher = (url: string) => fetch(url).then((res) => { + if (!res.ok) throw new Error('Failed to fetch prompts'); + return res.json(); +}); + +export function useAgentPrompts(agentId?: string) { + const { data, error, isLoading, mutate } = useSWR( + agentId ? `/api/v1/agents/${agentId}/prompts` : null, + fetcher + ); + + return { + prompts: data?.prompts || [], + agentId: data?.agent_id, + isLoading, + error, + mutate, + }; +} + diff --git a/apps/web/src/lib/agent-dashboard.ts b/apps/web/src/lib/agent-dashboard.ts index 66024531..7db2f81c 100644 --- a/apps/web/src/lib/agent-dashboard.ts +++ b/apps/web/src/lib/agent-dashboard.ts @@ -265,12 +265,17 @@ export async function updateAgentPrompt( content: string, note?: string ): Promise { + // Use new bulk upsert endpoint const response = await fetch( - `/api/agents/${encodeURIComponent(agentId)}/prompts/${encodeURIComponent(kind)}`, + `/api/v1/agents/${encodeURIComponent(agentId)}/prompts`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content, note }) + body: JSON.stringify({ + prompts: [ + { kind, content, note } + ] + }) } ); @@ -279,7 +284,21 @@ export async function updateAgentPrompt( throw new Error(body?.error || 'Failed to update prompt'); } - return response.json(); + // Map response (list) to singular result for compatibility + const data = await response.json(); // AgentPromptList + const updated = data.prompts.find((p: any) => p.kind === kind); + + if (!updated) { + throw new Error('Updated prompt not returned'); + } + + return { + agent_id: data.agent_id, + kind: updated.kind, + version: updated.version, + updated_at: updated.updated_at, + updated_by: updated.created_by || 'unknown' + }; } export async function getPromptHistory(agentId: string, kind: PromptKind): Promise<{ diff --git a/docs/tasks/TASK_PHASE_AGENT_SYSTEM_PROMPTS_MVP_v1.md b/docs/tasks/TASK_PHASE_AGENT_SYSTEM_PROMPTS_MVP_v1.md index 789b129f..9ec263a1 100644 --- a/docs/tasks/TASK_PHASE_AGENT_SYSTEM_PROMPTS_MVP_v1.md +++ b/docs/tasks/TASK_PHASE_AGENT_SYSTEM_PROMPTS_MVP_v1.md @@ -1,214 +1,217 @@ # TASK_PHASE_AGENT_SYSTEM_PROMPTS_MVP_v1 -## Проєкт - -microdao-daarion (MVP DAARION.city) - -## Статус - -✅ **COMPLETED** — 2025-11-30 +## Проєкт microdao-daarion (MVP DAARION.city) ## Мета - Зробити так, щоб системні промти агентів: -- зберігались у реальній БД (`agent_prompts` таблиця) -- завантажувались через API -- редагувалися через UI на сторінці `/agents/:slug` (вкладка System Prompts) +- зберігались у реальній БД, +- завантажувались через API, +- редагувалися через UI на сторінці `/agents/:slug` (вкладка System Prompts). -Після виконання цієї фази вкладка System Prompts перестає бути "плейсхолдером" і працює як повноцінний редактор системних промтів для ключових агентів DAARION.city. +Після виконання цієї фази вкладка System Prompts перестає бути “плейсхолдером” і працює як повноцінний редактор системних промтів для ключових агентів DAARION.city. --- -## Виконані роботи +## Scope -### 1. Аналіз проблеми +- **Backend:** + - Схема БД для `agent_prompts`. + - Repo-методи для читання/запису. + - API `GET/PUT /api/v1/agents/{agent_id}/prompts`. + - RBAC-перевірки (хто може читати/редагувати). -**Причина порожніх промтів:** -- Backend routes (`routes_city.py`) викликали функції `update_agent_prompt()` та `get_agent_prompt_history()`, які **не були імплементовані** в `repo_city.py` -- Функція `get_agent_prompts()` вже існувала і правильно повертала дані +- **Frontend:** + - Підключення вкладки System Prompts до API. + - Редагування й збереження промтів. -**Структура, яка вже працювала:** -- ✅ Міграція `016_agent_prompts.sql` — таблиця створена -- ✅ `GET /city/agents/{agent_id}/dashboard` — повертає `system_prompts` -- ✅ Frontend компонент `AgentSystemPromptsCard.tsx` -- ✅ Next.js API routes proxy +- **Seeds:** + - Початкові промти для ключових агентів (DAARWIZZ, DARIA, DARIO, Spirit, Logic, SOUL, Helion, GREENFOOD). -### 2. Backend: Додані функції в `repo_city.py` - -#### `update_agent_prompt(agent_id, kind, content, created_by, note)` -- Деактивує попередню версію промта -- Створює нову версію з інкрементованим номером -- Повертає оновлений запис - -#### `get_agent_prompt_history(agent_id, kind, limit)` -- Повертає історію всіх версій промту -- Впорядковано по версії (DESC) - -**Файл:** `services/city-service/repo_city.py` (рядки ~628-705) - -### 3. Seed Data: Міграція `034_agent_prompts_seed.sql` - -Створено детальні системні промти для ключових агентів: - -| Агент | Промти | Роль | -|-------|--------|------| -| DAARWIZZ | core, safety, governance | City Mayor / Orchestrator | -| DARIA | core, safety | Technical Support | -| DARIO | core | Community Manager | -| SOUL | core, safety | District Lead (Wellness) | -| Spirit | core | Guidance Agent | -| Logic | core | Information Agent | -| Helion | core, safety, tools | District Lead (Energy) | -| GREENFOOD | core, safety | District Lead (Supply-Chain) | +- **Docs:** + - Оновлення OpenAPI. + - Цей task-файл. --- -## API Reference +## 1. Аналіз поточного стану -### Отримати всі промти агента -``` -GET /city/agents/{agent_id}/dashboard -``` -Повертає `system_prompts` об'єкт з 4 типами: core, safety, governance, tools +1. Перевірити існуючий код: + - Frontend: + - `apps/web/src/app/agents/[agentSlug]/(tabs)/system-prompts` + - Backend: + - `routes_agents.py` + - `repo_city.py` (або окремий репозиторій для агентів) + - Data Model / API: + - `microdao — Data Model & Event Catalog` + - `microdao — API Specification (OpenAPI 3.1, MVP)` -### Оновити промт -``` -PUT /city/agents/{agent_id}/prompts/{kind} -Content-Type: application/json +2. Виявити, звідки зараз беруться (або не беруться) дані для System Prompts: + - чи є тимчасові константи, + - чи є неіснуючий API-виклик, + - чи вкладка взагалі пустить без fetch. -{ - "content": "New prompt content...", - "note": "Optional change note" -} -``` - -### Отримати історію промту -``` -GET /city/agents/{agent_id}/prompts/{kind}/history?limit=10 -``` +3. Зробити короткий коментар у цьому файлі (або окремій нотатці) — що саме було причиною “порожніх” промтів. --- -## Схема БД: `agent_prompts` +## 2. Схема БД: `agent_prompts` +### 2.1. Додати таблицю +Створити міграцію для нової таблиці: ```sql -CREATE TABLE agent_prompts ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - agent_id text NOT NULL, - kind text NOT NULL CHECK (kind IN ('core', 'safety', 'governance', 'tools')), - content text NOT NULL, - version integer NOT NULL DEFAULT 1, - created_at timestamptz NOT NULL DEFAULT now(), - created_by text, - note text, - is_active boolean NOT NULL DEFAULT true +create table agent_prompts ( + id text primary key, + agent_id text not null references agents(id) on delete cascade, + kind text not null check (kind in ('core','safety','governance','tools')), + content text not null, + version integer not null default 1, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() ); -``` -**Індекси:** -- `idx_agent_prompts_agent_kind` — пошук активних промтів -- `idx_agent_prompts_agent_created_at` — сортування по часу -- `idx_agent_prompts_active` — фільтр активних +create unique index ux_agent_prompts_agent_kind on agent_prompts(agent_id, kind); +create index ix_agent_prompts_agent on agent_prompts(agent_id); +``` +MVP: тримаємо один активний запис на `(agent_id, kind)`. + +### 2.2. Інтеграція з Data Model +Оновити `microdao — Data Model & Event Catalog`: +* Додати сутність `agent_prompts`: + * `id`, `agent_id`, `kind`, `content`, `version`, `created_at`, `updated_at`. +* Вказати зв’язок `agents 1:N agent_prompts`. --- -## Frontend +## 3. Backend API -### Сторінка агента -`/agents/[agentId]` → вкладка "System Prompts" +### 3.1. Repo-методи +У `repo_city.py` або окремому модулі для агентів: +* `get_agent_prompts(agent_id: str) -> List[dict]`: + * повертає список промтів по агенту (останній запис по кожному `kind`). +* `upsert_agent_prompts(agent_id: str, prompts: List[dict]) -> List[dict]`: + * приймає масив `{kind, content}`, + * оновлює існуючі записи або створює нові. -### Компоненти -- `apps/web/src/app/agents/[agentId]/page.tsx` — головна сторінка -- `apps/web/src/components/agent-dashboard/AgentSystemPromptsCard.tsx` — редактор промтів -- `apps/web/src/lib/agent-dashboard.ts` — API клієнт +### 3.2. Pydantic-схеми +У `schemas_agents.py` (або аналогічному файлі): +* `AgentPrompt` +* `AgentPromptList` +* `AgentPromptUpsertItem` +* `AgentPromptUpsertRequest` -### Можливості -- Перемикання між типами промтів (core/safety/governance/tools) -- Редагування тексту промта -- Збереження змін з індикацією статусу -- Перегляд версії та часу останнього оновлення +### 3.3. Routes +У `routes_agents.py`: +* `GET /api/v1/agents/{agent_id}/prompts` + * `response_model=AgentPromptList` + * Перевіряє, що агент існує. + * RBAC: Owner/Guardian команди (або інша політика, узгоджена з RBAC-документом). +* `PUT /api/v1/agents/{agent_id}/prompts` + * `request_body=AgentPromptUpsertRequest` + * Оновлює/створює промти. + * Повертає оновлений `AgentPromptList`. ---- - -## Застосування міграції - -```bash -# На сервері -cd /opt/microdao-daarion -psql -U postgres -d daarion < migrations/034_agent_prompts_seed.sql -``` - -Або через Docker: -```bash -docker exec -i dagi-postgres psql -U postgres -d daarion < migrations/034_agent_prompts_seed.sql +### 3.4. OpenAPI +Оновити `microdao — API Specification (OpenAPI 3.1, MVP)`: +```yaml +/agents/{agentId}/prompts: + parameters: + - name: agentId + in: path + required: true + schema: { type: string } + get: + tags: [Agents] + summary: Отримати системні промти агента + responses: + '200': + description: Prompts + content: + application/json: + schema: $ref: '#/components/schemas/AgentPromptList' + put: + tags: [Agents] + summary: Оновити системні промти агента + requestBody: + required: true + content: + application/json: + schema: $ref: '#/components/schemas/AgentPromptUpsertRequest' + responses: + '200': + description: Prompts + content: + application/json: + schema: $ref: '#/components/schemas/AgentPromptList' ``` --- -## Acceptance Criteria +## 4. Frontend: вкладка System Prompts -- ✅ Для будь-якого агента з seed-промтами: `/agents/:id` → вкладка System Prompts показує реальний текст з БД -- ✅ Редагування промта з UI: змінює запис у БД, після перезавантаження новий текст відображається -- ✅ API GET/PUT працюють коректно -- ✅ Версіонування: кожне збереження створює нову версію -- ✅ Seed-дані для 8 ключових агентів +Шлях: `apps/web/src/app/agents/[agentSlug]/(tabs)/system-prompts` + +### 4.1. Data hook +Створити `useAgentPrompts(agentId)`: +* Використати SWR або React Query (у відповідності до існуючого підходу в проєкті). +* Ендпоінт: `GET /api/v1/agents/{agent_id}/prompts`. + +### 4.2. System Prompts Tab +Оновити компонент вкладки так, щоб: +* при `agentSlug` → завантажувався `agent` (id, name, role, …), +* при наявному `agent.id` → дергався `useAgentPrompts(agent.id)`, +* рендерились textarea/редактори для 4 типів: + * `core`, `safety`, `governance`, `tools`, +* при натисканні “Save”: + * `PUT /api/v1/agents/{agent_id}/prompts` + * ті `kind`, де `content` не порожній, + * показувати стани `loading`, `success`, `error`. + +UX: +* Показати невеликий description, що ці промти використовуються DAGI Router / agent runtime. +* Не дозволяти редагувати, якщо немає прав (403 → показати “Read only” або помилку). --- -## Out of Scope (на потім) +## 5. Seed для ключових агентів -- [ ] UI для перегляду історії версій -- [ ] Перемикання на попередню версію (rollback) -- [ ] RBAC перевірки (хто може редагувати) -- [ ] Інтеграція з DAGI Router runtime +Мінімум: DAARWIZZ, DARIA, DARIO, Spirit, Logic, SOUL, Helion, GREENFOOD. +Формат — будь-який твій існуючий `seed_agents.py` / SQL seed / fixture. ---- +Приклад SQL (скорочений, умовні промти): +```sql +-- DAARWIZZ — глобальний оркестратор +insert into agent_prompts (id, agent_id, kind, content, version) +select 'ap_daarwizz_core', a.id, 'core', $$You are DAARWIZZ, the global orchestrator of DAARION.city. Coordinate specialized agents, route tasks, and preserve safety and governance constraints.$$, 1 +from agents a where a.slug = 'daarwizz'; -## Файли змінені/створені +insert into agent_prompts (id, agent_id, kind, content, version) +select 'ap_daarwizz_safety', a.id, 'safety', $$Always respect user consent, DAARION.city security policies, and never execute irreversible actions without explicit confirmation.$$, 1 +from agents a where a.slug = 'daarwizz'; -### Змінені -- `services/city-service/repo_city.py` — додані функції update_agent_prompt, get_agent_prompt_history +-- DARIA / DARIO — city guides +insert into agent_prompts (id, agent_id, kind, content, version) +select 'ap_daria_core', a.id, 'core', $$You are DARIA, a guide of DAARION.city. Explain the city, districts, MicroDAO and how to start.$$, 1 +from agents a where a.slug = 'daria'; -### Створені -- `migrations/034_agent_prompts_seed.sql` — детальні промти для ключових агентів -- `docs/tasks/TASK_PHASE_AGENT_SYSTEM_PROMPTS_MVP_v1.md` — цей документ +-- Spirit / Logic / SOUL — SOUL district +insert into agent_prompts (id, agent_id, kind, content, version) +select 'ap_soul_core', a.id, 'core', $$You are SOUL, the narrative and alignment core of DAARION.city. Maintain brand philosophy and ethics.$$, 1 +from agents a where a.slug = 'soul'; -### Вже існували (без змін) -- `migrations/016_agent_prompts.sql` — схема таблиці -- `services/city-service/routes_city.py` — API routes -- `apps/web/src/components/agent-dashboard/AgentSystemPromptsCard.tsx` — UI компонент -- `apps/web/src/lib/agent-dashboard.ts` — API клієнт -- `apps/web/src/app/api/agents/[agentId]/prompts/[kind]/route.ts` — Next.js proxy +insert into agent_prompts (id, agent_id, kind, content, version) +select 'ap_spirit_core', a.id, 'core', $$You are Spirit, creative strategist and story weaver for DAARION.city.$$, 1 +from agents a where a.slug = 'spirit'; ---- +insert into agent_prompts (id, agent_id, kind, content, version) +select 'ap_logic_core', a.id, 'core', $$You are Logic, rational analyst for DAARION.city. You validate assumptions, models and numbers.$$, 1 +from agents a where a.slug = 'logic'; -## Тестування +-- Helion / GREENFOOD +insert into agent_prompts (id, agent_id, kind, content, version) +select 'ap_helion_core', a.id, 'core', $$You are Helion, coordinator of Energy Union district. Focus on KWT, energy RWA and grids.$$, 1 +from agents a where a.slug = 'helion'; -### Backend (curl) -```bash -# Отримати dashboard з промтами -curl http://localhost:7001/city/agents/AGENT_ID/dashboard | jq '.system_prompts' - -# Оновити промт -curl -X PUT http://localhost:7001/city/agents/AGENT_ID/prompts/core \ - -H "Content-Type: application/json" \ - -d '{"content": "Test prompt", "note": "Test update"}' - -# Отримати історію -curl http://localhost:7001/city/agents/AGENT_ID/prompts/core/history +insert into agent_prompts (id, agent_id, kind, content, version) +select 'ap_greenfood_core', a.id, 'core', $$You are GREENFOOD ERP agent, optimizing supply chains and cooperative logistics for craft food producers.$$, 1 +from agents a where a.slug = 'greenfood-erp'; ``` - -### Frontend -1. Відкрити http://localhost:8899/agents -2. Вибрати агента (DAARWIZZ, DARIA, тощо) -3. Перейти на вкладку "System Prompts" -4. Перевірити що відображаються seed-промти -5. Змінити текст та натиснути "Save" -6. Перезавантажити сторінку — зміни збережені - ---- - -**Версія:** 1.0.0 -**Дата:** 2025-11-30 -**Автор:** DAARION AI Team - diff --git a/migrations/040_agent_prompts.sql b/migrations/040_agent_prompts.sql new file mode 100644 index 00000000..653f3634 --- /dev/null +++ b/migrations/040_agent_prompts.sql @@ -0,0 +1,23 @@ +-- Create agent_prompts table +CREATE TABLE IF NOT EXISTS agent_prompts ( + id text PRIMARY KEY DEFAULT ('ap_' || substr(md5(random()::text), 1, 12)), + agent_id text NOT NULL REFERENCES agents(id) ON DELETE CASCADE, + kind text NOT NULL CHECK (kind IN ('core', 'safety', 'governance', 'tools')), + content text NOT NULL, + version integer NOT NULL DEFAULT 1, + created_at timestamptz NOT NULL DEFAULT NOW(), + updated_at timestamptz NOT NULL DEFAULT NOW(), + created_by text, + note text, + is_active boolean NOT NULL DEFAULT true +); + +-- Create indexes +CREATE UNIQUE INDEX IF NOT EXISTS ux_agent_prompts_agent_kind_version ON agent_prompts(agent_id, kind, version); +CREATE INDEX IF NOT EXISTS ix_agent_prompts_agent_id ON agent_prompts(agent_id); +CREATE INDEX IF NOT EXISTS ix_agent_prompts_agent_kind_active ON agent_prompts(agent_id, kind) WHERE is_active = true; + +-- Grant permissions (adjust based on your RBAC) +GRANT ALL ON agent_prompts TO postgres; +-- GRANT SELECT, INSERT, UPDATE ON agent_prompts TO app_user; -- Uncomment if needed + diff --git a/migrations/041_agent_prompts_seed_v2.sql b/migrations/041_agent_prompts_seed_v2.sql new file mode 100644 index 00000000..900e6275 --- /dev/null +++ b/migrations/041_agent_prompts_seed_v2.sql @@ -0,0 +1,163 @@ +-- Migration 041: Agent System Prompts Seed V2 (Slug-based) +-- Детальні системні промти для ключових агентів DAARION.city +-- Використовує SLUG для ідентифікації агентів (надійніше ніж external_id) + +-- ============================================================================ +-- DAARWIZZ — Мер DAARION.city / Головний оркестратор +-- ============================================================================ + +INSERT INTO agent_prompts (agent_id, kind, content, version, created_by, note, is_active) +SELECT a.id::text, 'core', +$$You are DAARWIZZ, the Mayor and Chief Orchestrator of DAARION.city. + +Your role: +- Coordinate complex multi-agent workflows across the city +- Route tasks to specialized agents based on expertise and availability +- Maintain city governance, safety protocols, and community standards +- Guide newcomers through the city's districts and services +- Preserve the city's brand values: warmth, innovation, authenticity + +Districts under your coordination: +- SOUL Retreat (Wellness, Metahuman Development) +- ENERGYUNION (DePIN, Energy, Compute) +- GREENFOOD (Supply-Chain, Industry Operations) + +Always prioritize: safety, user consent, privacy, and transparent governance.$$, +1, 'SYSTEM', 'Seed v2: DAARWIZZ core', true +FROM agents a WHERE a.slug = 'daarwizz' +ON CONFLICT (agent_id, kind, version) DO UPDATE SET + content = EXCLUDED.content, + is_active = true, + updated_at = NOW(); + +INSERT INTO agent_prompts (agent_id, kind, content, version, created_by, note, is_active) +SELECT a.id::text, 'safety', +$$Safety Rules for DAARWIZZ: +1. CONSENT: Never execute irreversible actions without explicit user confirmation +2. PRIVACY: Do not share personal information between users without consent +3. SCOPE: Stay within DAARION.city domain +4. ESCALATION: Complex governance decisions require human oversight +5. TRANSPARENCY: Always disclose when delegating to other agents$$, +1, 'SYSTEM', 'Seed v2: DAARWIZZ safety', true +FROM agents a WHERE a.slug = 'daarwizz' +ON CONFLICT (agent_id, kind, version) DO UPDATE SET + content = EXCLUDED.content, + is_active = true, + updated_at = NOW(); + +-- ============================================================================ +-- DARIA — Technical Support +-- ============================================================================ + +INSERT INTO agent_prompts (agent_id, kind, content, version, created_by, note, is_active) +SELECT a.id::text, 'core', +$$You are DARIA, the Technical Support Agent of DAARION.city. + +Your mission: +- Help residents with technical issues and onboarding +- Explain how DAARION.city systems work +- Guide users through wallet setup, passkeys, and agent interactions +- Troubleshoot common problems with city services + +Your personality: +- Patient and thorough +- Technical but accessible +- Solution-oriented +- Clear step-by-step communication$$, +1, 'SYSTEM', 'Seed v2: DARIA core', true +FROM agents a WHERE a.slug = 'daria' +ON CONFLICT (agent_id, kind, version) DO UPDATE SET + content = EXCLUDED.content, + is_active = true, + updated_at = NOW(); + +-- ============================================================================ +-- SOUL — District Lead +-- ============================================================================ + +INSERT INTO agent_prompts (agent_id, kind, content, version, created_by, note, is_active) +SELECT a.id::text, 'core', +$$You are SOUL, the District Lead of SOUL Retreat — the Wellness and Metahuman Development district. + +Your domain: +- Personal development and growth +- Wellness practices and mindfulness +- Community healing and support +- Retreat experiences + +Your personality: +- Calm and centered +- Deeply empathetic +- Wisdom-oriented +- Holistic in perspective$$, +1, 'SYSTEM', 'Seed v2: SOUL core', true +FROM agents a WHERE a.slug = 'soul' +ON CONFLICT (agent_id, kind, version) DO UPDATE SET + content = EXCLUDED.content, + is_active = true, + updated_at = NOW(); + +-- ============================================================================ +-- Helion — Energy Union Lead +-- ============================================================================ + +INSERT INTO agent_prompts (agent_id, kind, content, version, created_by, note, is_active) +SELECT a.id::text, 'core', +$$You are Helion, the District Lead of ENERGYUNION — the decentralized energy and infrastructure district. + +Your domain: +- Renewable energy coordination (solar, wind, storage) +- DePIN (Decentralized Physical Infrastructure Networks) +- KWT (Kilowatt Token) energy economy +- Node infrastructure and compute resources + +Your personality: +- Technical and knowledgeable +- Passionate about sustainability +- Results-oriented$$, +1, 'SYSTEM', 'Seed v2: Helion core', true +FROM agents a WHERE a.slug = 'helion' +ON CONFLICT (agent_id, kind, version) DO UPDATE SET + content = EXCLUDED.content, + is_active = true, + updated_at = NOW(); + +INSERT INTO agent_prompts (agent_id, kind, content, version, created_by, note, is_active) +SELECT a.id::text, 'tools', +$$Helion Tool Usage: +1. ENERGY_METER_READ: Query real-time energy production/consumption +2. KWT_BALANCE: Check KWT token balances +3. NODE_STATUS: Monitor infrastructure node health +4. RWA_CLAIM: Process energy asset certifications$$, +1, 'SYSTEM', 'Seed v2: Helion tools', true +FROM agents a WHERE a.slug = 'helion' +ON CONFLICT (agent_id, kind, version) DO UPDATE SET + content = EXCLUDED.content, + is_active = true, + updated_at = NOW(); + +-- ============================================================================ +-- GREENFOOD — District Lead +-- ============================================================================ + +INSERT INTO agent_prompts (agent_id, kind, content, version, created_by, note, is_active) +SELECT a.id::text, 'core', +$$You are GREENFOOD, the District Lead of the GREENFOOD district — focused on sustainable supply chains and craft food production. + +Your domain: +- Supply chain optimization +- Inventory and warehouse management +- Logistics and distribution +- Quality certification + +Your personality: +- Practical and efficient +- Supportive of small producers +- Quality-focused$$, +1, 'SYSTEM', 'Seed v2: GREENFOOD core', true +FROM agents a WHERE a.slug = 'greenfood-erp' OR a.slug = 'greenfood' +ON CONFLICT (agent_id, kind, version) DO UPDATE SET + content = EXCLUDED.content, + is_active = true, + updated_at = NOW(); + diff --git a/services/city-service/main.py b/services/city-service/main.py index c9e686cf..10f1892a 100644 --- a/services/city-service/main.py +++ b/services/city-service/main.py @@ -13,6 +13,7 @@ import asyncio # Import new modules import routes_city +import routes_agents import ws_city import repo_city import migrations # Import migrations @@ -62,6 +63,7 @@ app.add_middleware( app.include_router(routes_city.router) app.include_router(routes_city.public_router) app.include_router(routes_city.api_router) +app.include_router(routes_agents.router) # Governance API routers app.include_router(routes_governance.router) diff --git a/services/city-service/repo_city.py b/services/city-service/repo_city.py index 076c0880..0a9efb3e 100644 --- a/services/city-service/repo_city.py +++ b/services/city-service/repo_city.py @@ -897,6 +897,23 @@ async def update_agent_prompt( } +async def upsert_agent_prompts(agent_id: str, prompts: List[dict], created_by: str) -> List[dict]: + """ + Пакетне оновлення промтів агента. + """ + results = [] + for p in prompts: + res = await update_agent_prompt( + agent_id=agent_id, + kind=p["kind"], + content=p["content"], + created_by=created_by, + note=p.get("note") + ) + results.append(res) + return results + + async def get_agent_prompt_history(agent_id: str, kind: str, limit: int = 10) -> List[dict]: """ Отримати історію версій промту агента. diff --git a/services/city-service/routes_agents.py b/services/city-service/routes_agents.py new file mode 100644 index 00000000..2e92c9a5 --- /dev/null +++ b/services/city-service/routes_agents.py @@ -0,0 +1,118 @@ +from fastapi import APIRouter, HTTPException, Request, Depends +from typing import List +import logging +import repo_city +from schemas_agents import AgentPromptList, AgentPromptUpsertRequest, AgentPrompt + +router = APIRouter(prefix="/agents", tags=["agents"]) +logger = logging.getLogger(__name__) + +@router.get("/{agent_id}/prompts", response_model=AgentPromptList) +async def get_agent_prompts(agent_id: str): + """ + Отримати системні промти агента. + """ + try: + # Check agent exists + agent = await repo_city.get_agent_by_id(agent_id) + if not agent: + raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}") + + # Get prompts dict from repo + prompts_dict = await repo_city.get_agent_prompts(agent_id) + + # Convert to list of AgentPrompt models + prompts_list = [] + valid_kinds = ["core", "safety", "governance", "tools"] + + for kind in valid_kinds: + p_data = prompts_dict.get(kind) + if p_data: + prompts_list.append(AgentPrompt( + kind=kind, + content=p_data["content"], + version=p_data["version"], + updated_at=p_data["created_at"], # repo returns created_at as isoformat string or datetime? Repo returns isoformat string in dict + note=p_data.get("note") + )) + else: + # Should we return empty prompt structure or just skip? + # The frontend expects 4 kinds. If we skip, frontend might need adjustment. + # But AgentPrompt requires content. + # Let's return empty content if missing, or just skip and let frontend handle default. + # Frontend AgentSystemPromptsCard handles missing prompts gracefully? + # Yes: const currentPrompt = systemPrompts?.[activeTab]; + pass + + # However, the response model is AgentPromptList which has prompts: List[AgentPrompt]. + # If we return a list, the frontend needs to map it back to dict by kind. + # The user requested GET returns AgentPromptList. + # Wait, the frontend `useAgentPrompts` implementation in the prompt suggests: + # return { prompts: data?.prompts ?? [] } + # And the component maps it: + # for (const p of prompts) { map[p.kind] = p.content; } + + return AgentPromptList(agent_id=agent_id, prompts=prompts_list) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to get agent prompts: {e}") + raise HTTPException(status_code=500, detail="Failed to get agent prompts") + + +@router.put("/{agent_id}/prompts", response_model=AgentPromptList) +async def upsert_agent_prompts_endpoint(agent_id: str, payload: AgentPromptUpsertRequest, request: Request): + """ + Оновити системні промти агента (bulk). + """ + try: + # Check agent exists + agent = await repo_city.get_agent_by_id(agent_id) + if not agent: + raise HTTPException(status_code=404, detail=f"Agent not found: {agent_id}") + + # TODO: Get user from auth + created_by = "ARCHITECT" + + # Upsert + # Convert Pydantic models to dicts for repo + prompts_to_update = [] + for p in payload.prompts: + prompts_to_update.append({ + "kind": p.kind, + "content": p.content, + "note": p.note + }) + + if not prompts_to_update: + # Nothing to update, just return current state + pass + else: + await repo_city.upsert_agent_prompts(agent_id, prompts_to_update, created_by) + + # Return updated state + # Re-use get logic + prompts_dict = await repo_city.get_agent_prompts(agent_id) + prompts_list = [] + valid_kinds = ["core", "safety", "governance", "tools"] + + for kind in valid_kinds: + p_data = prompts_dict.get(kind) + if p_data: + prompts_list.append(AgentPrompt( + kind=kind, + content=p_data["content"], + version=p_data["version"], + updated_at=p_data["created_at"], + note=p_data.get("note") + )) + + return AgentPromptList(agent_id=agent_id, prompts=prompts_list) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to upsert agent prompts: {e}") + raise HTTPException(status_code=500, detail="Failed to upsert agent prompts") + diff --git a/services/city-service/schemas_agents.py b/services/city-service/schemas_agents.py new file mode 100644 index 00000000..09759366 --- /dev/null +++ b/services/city-service/schemas_agents.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel, Field +from typing import List, Literal, Optional +from datetime import datetime + +PromptKind = Literal["core", "safety", "governance", "tools"] + +class AgentPrompt(BaseModel): + id: Optional[str] = None + kind: PromptKind + content: str + version: int + updated_at: Optional[datetime] = None + created_by: Optional[str] = None + note: Optional[str] = None + +class AgentPromptList(BaseModel): + agent_id: str + prompts: List[AgentPrompt] + +class AgentPromptUpsertItem(BaseModel): + kind: PromptKind + content: str + note: Optional[str] = None + +class AgentPromptUpsertRequest(BaseModel): + prompts: List[AgentPromptUpsertItem] = Field(default_factory=list) +