diff --git a/README.md b/README.md index d3c5eb1..9e9100d 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,20 @@ docker compose up -d # picks up the changed .env — five agents in series building a classifier on a public dataset. It picks up exactly where this step leaves off. +## Persona library + +Add a Persona node and choose a built-in preset or a saved persona in its +inspector. Uploading a plain `.md` file saves a reusable library entry named +from the filename and copies its text into the node or factor level. No +frontmatter, description, or personality metadata is required. The original +local file is unchanged. + +Use **Manage personas / Save current** in the node inspector or persona factor +editor to save typed instructions, edit library entries, or delete them. +Existing nodes and factor levels retain their copied text; choose an entry +again to use its latest revision. Published experiments keep their frozen +persona text even if the library entry is later edited or deleted. + ## Measurements and Results The Design panel offers built-in runtime and supervised-ML metrics with their diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index a6fdb7d..dc685bd 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -33,6 +33,7 @@ import type { OkfBundle, OkfDocument } from '@/types/okf' import type { CellRunBatch, DatasetRowSchema, PromptPreview, Protocol, ProtocolGraph, ProtocolRevision, ProtocolRun, TestRun } from '@/types/protocols' import type { Run, RunStep } from '@/types/runs' import type { Skill, SkillListResponse, SkillUrlPreview } from '@/types/skills' +import type { Persona } from '@/types/personas' import type { AttemptDetail, AttemptPage, CursorPage, ExecutionCell, ExecutionSlot, ExecutionSummary, ResultJob } from '@/types/executionResults' const ACCESS_TOKEN_KEY = 'asaree_access_token' @@ -189,7 +190,7 @@ export interface ResultsScope { protocol_revision_id?: string } -export interface RowSelection { row_index?: number | null; sub_agent_selections?: Record; script_selections?: Record; tool_selections?: Record; skill_selections?: Record; dataset_selections?: Record; knowledge_selections?: Record } +export interface RowSelection { row_index?: number | null; sub_agent_selections?: Record; script_selections?: Record; tool_selections?: Record; skill_selections?: Record; persona_selections?: Record; dataset_selections?: Record; knowledge_selections?: Record } function scopeQuery(scope?: ResultsScope): string { const params = new URLSearchParams() @@ -497,6 +498,20 @@ export const mcpServersApi = { remove: (id: string) => request(`/mcp-servers/${id}`, { method: 'DELETE' }), } +export const personasApi = { + list: () => request('/personas'), + create: (data: { prompt: string; name?: string; description?: string | null; traits?: Record }) => + request('/personas', { method: 'POST', body: data }), + upload: (file: File) => { + const form = new FormData() + form.set('file', file) + return request('/personas/upload', { method: 'POST', body: form }) + }, + update: (id: string, data: { expected_revision: number; name?: string; prompt?: string; description?: string | null; traits?: Record }) => + request(`/personas/${id}`, { method: 'PATCH', body: data }), + remove: (id: string) => request(`/personas/${id}`, { method: 'DELETE' }), +} + export const skillsApi = { // The caller's own skills plus any global system skill -- GET /skills // returns {items,total}, unwrapped here so callers get a plain array like diff --git a/frontend/src/components/protocol/AddNodePanel.tsx b/frontend/src/components/protocol/AddNodePanel.tsx index 3cc71ee..3fb3ef4 100644 --- a/frontend/src/components/protocol/AddNodePanel.tsx +++ b/frontend/src/components/protocol/AddNodePanel.tsx @@ -1,7 +1,11 @@ +import { PERSONA_BROWSE } from './personaCatalog' import { useState } from 'react' import { ArrowRight, Atom, BookMarked, Bot, Braces, BrainCircuit, Cloud, Code2, Cog, Database, FileText, HardDrive, Repeat2, Route, ScrollText, ShieldCheck, Server, Sparkles, User, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { Card } from '@/components/ui/card' +import { nodeAccent } from '@/lib/nodeAccent' +import { cardAccent } from '@/lib/utils' import { DATASET_BROWSE } from './datasetCatalog' import { MCP_SERVER_BROWSE } from './mcpServerCatalog' import { OKF_BUNDLE_BROWSE, OKF_DOCUMENT_BROWSE } from './okfCatalog' @@ -17,6 +21,16 @@ const NODE_CATEGORIES = [ type NodeCategory = (typeof NODE_CATEGORIES)[number]['id'] +// Library entries create nodes of these kinds; all model providers share a hue. +const CATALOG_ACCENT_KINDS: Record = { + [PERSONA_BROWSE]: 'persona', + [DATASET_BROWSE]: 'dataset', + [MCP_SERVER_BROWSE]: 'mcp_tool', + [SKILL_BROWSE]: 'skill', + [OKF_BUNDLE_BROWSE]: 'okf_bundle', + [OKF_DOCUMENT_BROWSE]: 'okf_document', +} + // Every entry here earns its place: GET /api/mcp-servers already backs the // tool picker, GET /datasets already backs the dataset picker, // services.protocol_execution._run_gated_worker already implements the @@ -108,10 +122,10 @@ const NODE_CATALOG = [ icon: BrainCircuit, }, { - type: 'persona', + type: PERSONA_BROWSE, category: 'knowledge', - label: 'Persona', - description: 'OCEAN personality traits for experimental manipulation of agent behavior', + label: 'Personas', + description: 'Browse persona instructions for an Agent', icon: User, }, // Not a node type either -- picking this opens the dataset browser @@ -207,7 +221,7 @@ export function AddNodePanel({ }) return ( -
+

{title}

setQuery(e.target.value)} /> -
+
{filtered.length === 0 &&

No matching node types.

}
{groups.map((group) => ( @@ -225,23 +239,28 @@ export function AddNodePanel({
{group.items.map((item) => ( - + + ))}
diff --git a/frontend/src/components/protocol/DatasetBrowserPanel.tsx b/frontend/src/components/protocol/DatasetBrowserPanel.tsx index 030ba63..b5322d9 100644 --- a/frontend/src/components/protocol/DatasetBrowserPanel.tsx +++ b/frontend/src/components/protocol/DatasetBrowserPanel.tsx @@ -1,3 +1,6 @@ +import { Card } from '@/components/ui/card' +import { nodeAccent } from '@/lib/nodeAccent' +import { cardAccent } from '@/lib/utils' import { useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { ArrowLeft, Database, Plus, Trash2, X } from 'lucide-react' @@ -64,7 +67,7 @@ export function DatasetBrowserPanel({ ) return ( -
+
)} diff --git a/frontend/src/components/protocol/DesignTab.tsx b/frontend/src/components/protocol/DesignTab.tsx index b7d56d6..3d3ccfc 100644 --- a/frontend/src/components/protocol/DesignTab.tsx +++ b/frontend/src/components/protocol/DesignTab.tsx @@ -1,3 +1,4 @@ +import { personaFactorIssues } from '@/lib/personaFactors' import { subAgentFactorIssues } from '@/lib/subAgentFactors' import { sharedGroupIssues } from '@/lib/sharedFactors' import { knowledgeFactorIssues, isKnowledgeFactor } from '@/lib/knowledgeFactors' @@ -527,6 +528,7 @@ export function DesignTab({ const toolLibrary = useQuery({ queryKey: ['mcp-servers'], queryFn: () => mcpServersApi.list(), enabled: factors.some((factor) => factor.level_type === 'tool_selection' || factor.level_type === 'tool_toggle') }) const scriptIssues = scriptFactorIssues(draftGraph, factors) const toolIssues = toolFactorIssues(draftGraph, factors, toolLibrary.data ? new Set(toolLibrary.data.map((server) => server.id)) : undefined) + const personaIssues = personaFactorIssues(draftGraph, factors) const skillIssues = skillFactorIssues(draftGraph, factors, skillLibrary.data ? new Set(skillLibrary.data.map((skill) => skill.id)) : undefined) const impact = impactQuery.data @@ -896,14 +898,14 @@ export function DesignTab({ Unbound factor{unboundFactors.length === 1 ? '' : 's'}: {unboundFactors.join(', ')}. Rebind on the canvas or remove from this design.

)} - {[...patternIssues, ...scriptIssues, ...toolIssues, ...subAgentIssues, ...skillIssues, ...datasetIssues, ...knowledgeIssues].map((issue) =>

{issue}

)} + {[...patternIssues, ...scriptIssues, ...toolIssues, ...subAgentIssues, ...skillIssues, ...personaIssues, ...datasetIssues, ...knowledgeIssues].map((issue) =>

{issue}

)} - )} -
- ))} -
- {selectedPersonas.length < 5 && ( - - )} -
+ onChange({ ...value, ...next })} />
- -
- - -
+ + + { + const file = event.target.files?.[0] + if (file) upload.mutate(file, { onSuccess: persona => onChange({ ...value, ...configForSavedPersona(persona), mode: 'file' }) }) + event.target.value = '' + }} /> +

Upload plain Markdown; no frontmatter is required. The persona is saved to your library and its text is copied into this level.

+ {upload.error &&

{upload.error.message}

}
- -