feat(agent): Introduce identity profile for agents with normalization and default values
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
"""Add agent identity profile.
|
||||||
|
|
||||||
|
Revision ID: f7b3d0a1c9e2
|
||||||
|
Revises: c1c8b3b9f4d1
|
||||||
|
Create Date: 2026-02-04 22:45:00.000000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "f7b3d0a1c9e2"
|
||||||
|
down_revision = "c1c8b3b9f4d1"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"agents",
|
||||||
|
sa.Column("identity_profile", sa.JSON(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("agents", "identity_profile")
|
||||||
@@ -38,6 +38,27 @@ OFFLINE_AFTER = timedelta(minutes=10)
|
|||||||
AGENT_SESSION_PREFIX = "agent"
|
AGENT_SESSION_PREFIX = "agent"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_identity_profile(
|
||||||
|
profile: dict[str, object] | None,
|
||||||
|
) -> dict[str, str] | None:
|
||||||
|
if not profile:
|
||||||
|
return None
|
||||||
|
normalized: dict[str, str] = {}
|
||||||
|
for key, raw in profile.items():
|
||||||
|
if raw is None:
|
||||||
|
continue
|
||||||
|
if isinstance(raw, list):
|
||||||
|
parts = [str(item).strip() for item in raw if str(item).strip()]
|
||||||
|
if not parts:
|
||||||
|
continue
|
||||||
|
normalized[key] = ", ".join(parts)
|
||||||
|
continue
|
||||||
|
value = str(raw).strip()
|
||||||
|
if value:
|
||||||
|
normalized[key] = value
|
||||||
|
return normalized or None
|
||||||
|
|
||||||
|
|
||||||
def _slugify(value: str) -> str:
|
def _slugify(value: str) -> str:
|
||||||
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
||||||
return slug or uuid4().hex
|
return slug or uuid4().hex
|
||||||
@@ -176,6 +197,9 @@ async def create_agent(
|
|||||||
data["identity_template"] = None
|
data["identity_template"] = None
|
||||||
if data.get("soul_template") == "":
|
if data.get("soul_template") == "":
|
||||||
data["soul_template"] = None
|
data["soul_template"] = None
|
||||||
|
data["identity_profile"] = _normalize_identity_profile(
|
||||||
|
data.get("identity_profile")
|
||||||
|
)
|
||||||
agent = Agent.model_validate(data)
|
agent = Agent.model_validate(data)
|
||||||
agent.status = "provisioning"
|
agent.status = "provisioning"
|
||||||
raw_token = generate_agent_token()
|
raw_token = generate_agent_token()
|
||||||
@@ -267,6 +291,10 @@ async def update_agent(
|
|||||||
updates["identity_template"] = None
|
updates["identity_template"] = None
|
||||||
if updates.get("soul_template") == "":
|
if updates.get("soul_template") == "":
|
||||||
updates["soul_template"] = None
|
updates["soul_template"] = None
|
||||||
|
if "identity_profile" in updates:
|
||||||
|
updates["identity_profile"] = _normalize_identity_profile(
|
||||||
|
updates.get("identity_profile")
|
||||||
|
)
|
||||||
if not updates:
|
if not updates:
|
||||||
return _with_computed_status(agent)
|
return _with_computed_status(agent)
|
||||||
if "board_id" in updates:
|
if "board_id" in updates:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Agent(SQLModel, table=True):
|
|||||||
openclaw_session_id: str | None = Field(default=None, index=True)
|
openclaw_session_id: str | None = Field(default=None, index=True)
|
||||||
agent_token_hash: str | None = Field(default=None, index=True)
|
agent_token_hash: str | None = Field(default=None, index=True)
|
||||||
heartbeat_config: dict[str, Any] | None = Field(default=None, sa_column=Column(JSON))
|
heartbeat_config: dict[str, Any] | None = Field(default=None, sa_column=Column(JSON))
|
||||||
|
identity_profile: dict[str, Any] | None = Field(default=None, sa_column=Column(JSON))
|
||||||
identity_template: str | None = Field(default=None, sa_column=Column(Text))
|
identity_template: str | None = Field(default=None, sa_column=Column(Text))
|
||||||
soul_template: str | None = Field(default=None, sa_column=Column(Text))
|
soul_template: str | None = Field(default=None, sa_column=Column(Text))
|
||||||
provision_requested_at: datetime | None = Field(default=None)
|
provision_requested_at: datetime | None = Field(default=None)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class AgentBase(SQLModel):
|
|||||||
name: str
|
name: str
|
||||||
status: str = "provisioning"
|
status: str = "provisioning"
|
||||||
heartbeat_config: dict[str, Any] | None = None
|
heartbeat_config: dict[str, Any] | None = None
|
||||||
|
identity_profile: dict[str, Any] | None = None
|
||||||
identity_template: str | None = None
|
identity_template: str | None = None
|
||||||
soul_template: str | None = None
|
soul_template: str | None = None
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ class AgentUpdate(SQLModel):
|
|||||||
name: str | None = None
|
name: str | None = None
|
||||||
status: str | None = None
|
status: str | None = None
|
||||||
heartbeat_config: dict[str, Any] | None = None
|
heartbeat_config: dict[str, Any] | None = None
|
||||||
|
identity_profile: dict[str, Any] | None = None
|
||||||
identity_template: str | None = None
|
identity_template: str | None = None
|
||||||
soul_template: str | None = None
|
soul_template: str | None = None
|
||||||
|
|
||||||
@@ -44,4 +46,3 @@ class AgentHeartbeat(SQLModel):
|
|||||||
class AgentHeartbeatCreate(AgentHeartbeat):
|
class AgentHeartbeatCreate(AgentHeartbeat):
|
||||||
name: str
|
name: str
|
||||||
board_id: UUID | None = None
|
board_id: UUID | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ from app.models.gateways import Gateway
|
|||||||
from app.models.users import User
|
from app.models.users import User
|
||||||
|
|
||||||
DEFAULT_HEARTBEAT_CONFIG = {"every": "10m", "target": "none"}
|
DEFAULT_HEARTBEAT_CONFIG = {"every": "10m", "target": "none"}
|
||||||
|
DEFAULT_IDENTITY_PROFILE = {
|
||||||
|
"role": "Generalist",
|
||||||
|
"communication_style": "direct, concise, practical",
|
||||||
|
"emoji": ":gear:",
|
||||||
|
}
|
||||||
|
|
||||||
|
IDENTITY_PROFILE_FIELDS = {
|
||||||
|
"role": "identity_role",
|
||||||
|
"communication_style": "identity_communication_style",
|
||||||
|
"emoji": "identity_emoji",
|
||||||
|
}
|
||||||
|
|
||||||
DEFAULT_GATEWAY_FILES = frozenset(
|
DEFAULT_GATEWAY_FILES = frozenset(
|
||||||
{
|
{
|
||||||
@@ -94,6 +105,26 @@ def _build_context(
|
|||||||
session_key = agent.openclaw_session_id or ""
|
session_key = agent.openclaw_session_id or ""
|
||||||
base_url = settings.base_url or "REPLACE_WITH_BASE_URL"
|
base_url = settings.base_url or "REPLACE_WITH_BASE_URL"
|
||||||
main_session_key = gateway.main_session_key
|
main_session_key = gateway.main_session_key
|
||||||
|
identity_profile: dict[str, Any] = {}
|
||||||
|
if isinstance(agent.identity_profile, dict):
|
||||||
|
identity_profile = agent.identity_profile
|
||||||
|
normalized_identity: dict[str, str] = {}
|
||||||
|
for key, value in identity_profile.items():
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
if isinstance(value, list):
|
||||||
|
parts = [str(item).strip() for item in value if str(item).strip()]
|
||||||
|
if not parts:
|
||||||
|
continue
|
||||||
|
normalized_identity[key] = ", ".join(parts)
|
||||||
|
continue
|
||||||
|
text = str(value).strip()
|
||||||
|
if text:
|
||||||
|
normalized_identity[key] = text
|
||||||
|
identity_context = {
|
||||||
|
context_key: normalized_identity.get(field, DEFAULT_IDENTITY_PROFILE[field])
|
||||||
|
for field, context_key in IDENTITY_PROFILE_FIELDS.items()
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
"agent_name": agent.name,
|
"agent_name": agent.name,
|
||||||
"agent_id": agent_id,
|
"agent_id": agent_id,
|
||||||
@@ -110,6 +141,7 @@ def _build_context(
|
|||||||
"user_timezone": (user.timezone or "") if user else "",
|
"user_timezone": (user.timezone or "") if user else "",
|
||||||
"user_notes": (user.notes or "") if user else "",
|
"user_notes": (user.notes or "") if user else "",
|
||||||
"user_context": (user.context or "") if user else "",
|
"user_context": (user.context or "") if user else "",
|
||||||
|
**identity_context,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,17 @@ import { Input } from "@/components/ui/input";
|
|||||||
import SearchableSelect, {
|
import SearchableSelect, {
|
||||||
type SearchableSelectOption,
|
type SearchableSelectOption,
|
||||||
} from "@/components/ui/searchable-select";
|
} from "@/components/ui/searchable-select";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { getApiBaseUrl } from "@/lib/api-base";
|
import { getApiBaseUrl } from "@/lib/api-base";
|
||||||
import {
|
import {
|
||||||
DEFAULT_IDENTITY_TEMPLATE,
|
DEFAULT_IDENTITY_PROFILE,
|
||||||
DEFAULT_SOUL_TEMPLATE,
|
DEFAULT_SOUL_TEMPLATE,
|
||||||
} from "@/lib/agent-templates";
|
} from "@/lib/agent-templates";
|
||||||
|
|
||||||
@@ -29,6 +36,7 @@ type Agent = {
|
|||||||
every?: string;
|
every?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
identity_profile?: IdentityProfile | null;
|
||||||
identity_template?: string | null;
|
identity_template?: string | null;
|
||||||
soul_template?: string | null;
|
soul_template?: string | null;
|
||||||
};
|
};
|
||||||
@@ -39,6 +47,25 @@ type Board = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type IdentityProfile = {
|
||||||
|
role: string;
|
||||||
|
communication_style: string;
|
||||||
|
emoji: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMOJI_OPTIONS = [
|
||||||
|
{ value: ":gear:", label: "Gear", glyph: "⚙️" },
|
||||||
|
{ value: ":sparkles:", label: "Sparkles", glyph: "✨" },
|
||||||
|
{ value: ":rocket:", label: "Rocket", glyph: "🚀" },
|
||||||
|
{ value: ":megaphone:", label: "Megaphone", glyph: "📣" },
|
||||||
|
{ value: ":chart_with_upwards_trend:", label: "Growth", glyph: "📈" },
|
||||||
|
{ value: ":bulb:", label: "Idea", glyph: "💡" },
|
||||||
|
{ value: ":wrench:", label: "Builder", glyph: "🔧" },
|
||||||
|
{ value: ":shield:", label: "Shield", glyph: "🛡️" },
|
||||||
|
{ value: ":memo:", label: "Notes", glyph: "📝" },
|
||||||
|
{ value: ":brain:", label: "Brain", glyph: "🧠" },
|
||||||
|
];
|
||||||
|
|
||||||
const HEARTBEAT_TARGET_OPTIONS: SearchableSelectOption[] = [
|
const HEARTBEAT_TARGET_OPTIONS: SearchableSelectOption[] = [
|
||||||
{ value: "none", label: "None (no outbound message)" },
|
{ value: "none", label: "None (no outbound message)" },
|
||||||
{ value: "last", label: "Last channel" },
|
{ value: "last", label: "Last channel" },
|
||||||
@@ -50,6 +77,27 @@ const getBoardOptions = (boards: Board[]): SearchableSelectOption[] =>
|
|||||||
label: board.name,
|
label: board.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const normalizeIdentityProfile = (
|
||||||
|
profile: IdentityProfile
|
||||||
|
): IdentityProfile | null => {
|
||||||
|
const normalized: IdentityProfile = {
|
||||||
|
role: profile.role.trim(),
|
||||||
|
communication_style: profile.communication_style.trim(),
|
||||||
|
emoji: profile.emoji.trim(),
|
||||||
|
};
|
||||||
|
const hasValue = Object.values(normalized).some((value) => value.length > 0);
|
||||||
|
return hasValue ? normalized : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const withIdentityDefaults = (
|
||||||
|
profile: Partial<IdentityProfile> | null | undefined
|
||||||
|
): IdentityProfile => ({
|
||||||
|
role: profile?.role ?? DEFAULT_IDENTITY_PROFILE.role,
|
||||||
|
communication_style:
|
||||||
|
profile?.communication_style ?? DEFAULT_IDENTITY_PROFILE.communication_style,
|
||||||
|
emoji: profile?.emoji ?? DEFAULT_IDENTITY_PROFILE.emoji,
|
||||||
|
});
|
||||||
|
|
||||||
export default function EditAgentPage() {
|
export default function EditAgentPage() {
|
||||||
const { getToken, isSignedIn } = useAuth();
|
const { getToken, isSignedIn } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -63,9 +111,9 @@ export default function EditAgentPage() {
|
|||||||
const [boardId, setBoardId] = useState("");
|
const [boardId, setBoardId] = useState("");
|
||||||
const [heartbeatEvery, setHeartbeatEvery] = useState("10m");
|
const [heartbeatEvery, setHeartbeatEvery] = useState("10m");
|
||||||
const [heartbeatTarget, setHeartbeatTarget] = useState("none");
|
const [heartbeatTarget, setHeartbeatTarget] = useState("none");
|
||||||
const [identityTemplate, setIdentityTemplate] = useState(
|
const [identityProfile, setIdentityProfile] = useState<IdentityProfile>({
|
||||||
DEFAULT_IDENTITY_TEMPLATE
|
...DEFAULT_IDENTITY_PROFILE,
|
||||||
);
|
});
|
||||||
const [soulTemplate, setSoulTemplate] = useState(DEFAULT_SOUL_TEMPLATE);
|
const [soulTemplate, setSoulTemplate] = useState(DEFAULT_SOUL_TEMPLATE);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -111,9 +159,7 @@ export default function EditAgentPage() {
|
|||||||
if (data.heartbeat_config?.target) {
|
if (data.heartbeat_config?.target) {
|
||||||
setHeartbeatTarget(data.heartbeat_config.target);
|
setHeartbeatTarget(data.heartbeat_config.target);
|
||||||
}
|
}
|
||||||
setIdentityTemplate(
|
setIdentityProfile(withIdentityDefaults(data.identity_profile));
|
||||||
data.identity_template?.trim() || DEFAULT_IDENTITY_TEMPLATE
|
|
||||||
);
|
|
||||||
setSoulTemplate(data.soul_template?.trim() || DEFAULT_SOUL_TEMPLATE);
|
setSoulTemplate(data.soul_template?.trim() || DEFAULT_SOUL_TEMPLATE);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Something went wrong.");
|
setError(err instanceof Error ? err.message : "Something went wrong.");
|
||||||
@@ -168,7 +214,7 @@ export default function EditAgentPage() {
|
|||||||
every: heartbeatEvery.trim() || "10m",
|
every: heartbeatEvery.trim() || "10m",
|
||||||
target: heartbeatTarget,
|
target: heartbeatTarget,
|
||||||
},
|
},
|
||||||
identity_template: identityTemplate.trim() || null,
|
identity_profile: normalizeIdentityProfile(identityProfile),
|
||||||
soul_template: soulTemplate.trim() || null,
|
soul_template: soulTemplate.trim() || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -220,9 +266,10 @@ export default function EditAgentPage() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Agent identity
|
Basic configuration
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
<div className="mt-4 space-y-6">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Agent name <span className="text-red-500">*</span>
|
Agent name <span className="text-red-500">*</span>
|
||||||
@@ -234,6 +281,24 @@ export default function EditAgentPage() {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-900">
|
||||||
|
Role
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={identityProfile.role}
|
||||||
|
onChange={(event) =>
|
||||||
|
setIdentityProfile((current) => ({
|
||||||
|
...current,
|
||||||
|
role: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="e.g. Founder, Social Media Manager"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Board <span className="text-red-500">*</span>
|
Board <span className="text-red-500">*</span>
|
||||||
@@ -257,28 +322,55 @@ export default function EditAgentPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-900">
|
||||||
|
Emoji
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={identityProfile.emoji}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setIdentityProfile((current) => ({
|
||||||
|
...current,
|
||||||
|
emoji: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select emoji" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{EMOJI_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.glyph} {option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Agent persona
|
Personality & behavior
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
<div className="mt-4 space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Identity template
|
Communication style
|
||||||
</label>
|
</label>
|
||||||
<Textarea
|
<Input
|
||||||
value={identityTemplate}
|
value={identityProfile.communication_style}
|
||||||
onChange={(event) => setIdentityTemplate(event.target.value)}
|
onChange={(event) =>
|
||||||
rows={8}
|
setIdentityProfile((current) => ({
|
||||||
|
...current,
|
||||||
|
communication_style: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500">
|
|
||||||
Keep the agent_name and agent_id variables unchanged so
|
|
||||||
the gateway can render them correctly.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
@@ -296,7 +388,7 @@ export default function EditAgentPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Heartbeat settings
|
Schedule & notifications
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -12,10 +12,17 @@ import { Input } from "@/components/ui/input";
|
|||||||
import SearchableSelect, {
|
import SearchableSelect, {
|
||||||
type SearchableSelectOption,
|
type SearchableSelectOption,
|
||||||
} from "@/components/ui/searchable-select";
|
} from "@/components/ui/searchable-select";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { getApiBaseUrl } from "@/lib/api-base";
|
import { getApiBaseUrl } from "@/lib/api-base";
|
||||||
import {
|
import {
|
||||||
DEFAULT_IDENTITY_TEMPLATE,
|
DEFAULT_IDENTITY_PROFILE,
|
||||||
DEFAULT_SOUL_TEMPLATE,
|
DEFAULT_SOUL_TEMPLATE,
|
||||||
} from "@/lib/agent-templates";
|
} from "@/lib/agent-templates";
|
||||||
|
|
||||||
@@ -32,6 +39,25 @@ type Board = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type IdentityProfile = {
|
||||||
|
role: string;
|
||||||
|
communication_style: string;
|
||||||
|
emoji: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMOJI_OPTIONS = [
|
||||||
|
{ value: ":gear:", label: "Gear", glyph: "⚙️" },
|
||||||
|
{ value: ":sparkles:", label: "Sparkles", glyph: "✨" },
|
||||||
|
{ value: ":rocket:", label: "Rocket", glyph: "🚀" },
|
||||||
|
{ value: ":megaphone:", label: "Megaphone", glyph: "📣" },
|
||||||
|
{ value: ":chart_with_upwards_trend:", label: "Growth", glyph: "📈" },
|
||||||
|
{ value: ":bulb:", label: "Idea", glyph: "💡" },
|
||||||
|
{ value: ":wrench:", label: "Builder", glyph: "🔧" },
|
||||||
|
{ value: ":shield:", label: "Shield", glyph: "🛡️" },
|
||||||
|
{ value: ":memo:", label: "Notes", glyph: "📝" },
|
||||||
|
{ value: ":brain:", label: "Brain", glyph: "🧠" },
|
||||||
|
];
|
||||||
|
|
||||||
const HEARTBEAT_TARGET_OPTIONS: SearchableSelectOption[] = [
|
const HEARTBEAT_TARGET_OPTIONS: SearchableSelectOption[] = [
|
||||||
{ value: "none", label: "None (no outbound message)" },
|
{ value: "none", label: "None (no outbound message)" },
|
||||||
{ value: "last", label: "Last channel" },
|
{ value: "last", label: "Last channel" },
|
||||||
@@ -43,6 +69,18 @@ const getBoardOptions = (boards: Board[]): SearchableSelectOption[] =>
|
|||||||
label: board.name,
|
label: board.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const normalizeIdentityProfile = (
|
||||||
|
profile: IdentityProfile
|
||||||
|
): IdentityProfile | null => {
|
||||||
|
const normalized: IdentityProfile = {
|
||||||
|
role: profile.role.trim(),
|
||||||
|
communication_style: profile.communication_style.trim(),
|
||||||
|
emoji: profile.emoji.trim(),
|
||||||
|
};
|
||||||
|
const hasValue = Object.values(normalized).some((value) => value.length > 0);
|
||||||
|
return hasValue ? normalized : null;
|
||||||
|
};
|
||||||
|
|
||||||
export default function NewAgentPage() {
|
export default function NewAgentPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { getToken, isSignedIn } = useAuth();
|
const { getToken, isSignedIn } = useAuth();
|
||||||
@@ -52,9 +90,9 @@ export default function NewAgentPage() {
|
|||||||
const [boardId, setBoardId] = useState<string>("");
|
const [boardId, setBoardId] = useState<string>("");
|
||||||
const [heartbeatEvery, setHeartbeatEvery] = useState("10m");
|
const [heartbeatEvery, setHeartbeatEvery] = useState("10m");
|
||||||
const [heartbeatTarget, setHeartbeatTarget] = useState("none");
|
const [heartbeatTarget, setHeartbeatTarget] = useState("none");
|
||||||
const [identityTemplate, setIdentityTemplate] = useState(
|
const [identityProfile, setIdentityProfile] = useState<IdentityProfile>({
|
||||||
DEFAULT_IDENTITY_TEMPLATE
|
...DEFAULT_IDENTITY_PROFILE,
|
||||||
);
|
});
|
||||||
const [soulTemplate, setSoulTemplate] = useState(DEFAULT_SOUL_TEMPLATE);
|
const [soulTemplate, setSoulTemplate] = useState(DEFAULT_SOUL_TEMPLATE);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -113,7 +151,7 @@ export default function NewAgentPage() {
|
|||||||
every: heartbeatEvery.trim() || "10m",
|
every: heartbeatEvery.trim() || "10m",
|
||||||
target: heartbeatTarget,
|
target: heartbeatTarget,
|
||||||
},
|
},
|
||||||
identity_template: identityTemplate.trim() || null,
|
identity_profile: normalizeIdentityProfile(identityProfile),
|
||||||
soul_template: soulTemplate.trim() || null,
|
soul_template: soulTemplate.trim() || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -166,9 +204,10 @@ export default function NewAgentPage() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Agent identity
|
Basic configuration
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
<div className="mt-4 space-y-6">
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Agent name <span className="text-red-500">*</span>
|
Agent name <span className="text-red-500">*</span>
|
||||||
@@ -180,6 +219,24 @@ export default function NewAgentPage() {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-900">
|
||||||
|
Role
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={identityProfile.role}
|
||||||
|
onChange={(event) =>
|
||||||
|
setIdentityProfile((current) => ({
|
||||||
|
...current,
|
||||||
|
role: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="e.g. Founder, Social Media Manager"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Board <span className="text-red-500">*</span>
|
Board <span className="text-red-500">*</span>
|
||||||
@@ -203,28 +260,55 @@ export default function NewAgentPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-slate-900">
|
||||||
|
Emoji
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={identityProfile.emoji}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setIdentityProfile((current) => ({
|
||||||
|
...current,
|
||||||
|
emoji: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select emoji" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{EMOJI_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.glyph} {option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Agent persona
|
Personality & behavior
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
<div className="mt-4 space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Identity template
|
Communication style
|
||||||
</label>
|
</label>
|
||||||
<Textarea
|
<Input
|
||||||
value={identityTemplate}
|
value={identityProfile.communication_style}
|
||||||
onChange={(event) => setIdentityTemplate(event.target.value)}
|
onChange={(event) =>
|
||||||
rows={8}
|
setIdentityProfile((current) => ({
|
||||||
|
...current,
|
||||||
|
communication_style: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-slate-500">
|
|
||||||
Keep the agent_name and agent_id variables unchanged so
|
|
||||||
the gateway can render them correctly.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
@@ -242,7 +326,7 @@ export default function NewAgentPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Heartbeat settings
|
Schedule & notifications
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
export const DEFAULT_IDENTITY_TEMPLATE = `# IDENTITY.md
|
export const DEFAULT_IDENTITY_PROFILE = {
|
||||||
|
role: "Generalist",
|
||||||
Name: {{ agent_name }}
|
communication_style: "direct, concise, practical",
|
||||||
|
emoji: ":gear:",
|
||||||
Agent ID: {{ agent_id }}
|
};
|
||||||
|
|
||||||
Creature: AI
|
|
||||||
|
|
||||||
Vibe: calm, precise, helpful
|
|
||||||
|
|
||||||
Emoji: :gear:
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const DEFAULT_SOUL_TEMPLATE = `# SOUL.md
|
export const DEFAULT_SOUL_TEMPLATE = `# SOUL.md
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# IDENTITY.md
|
# IDENTITY.md — Who Am I?
|
||||||
|
|
||||||
Name: {{ agent_name }}
|
Name: {{ agent_name }}
|
||||||
|
|
||||||
Agent ID: {{ agent_id }}
|
Agent ID: {{ agent_id }}
|
||||||
|
|
||||||
Creature: AI
|
Creature: {{ identity_role }}
|
||||||
|
|
||||||
Vibe: calm, precise, helpful
|
Vibe: {{ identity_communication_style }}
|
||||||
|
|
||||||
Emoji: :gear:
|
Emoji: {{ identity_emoji }}
|
||||||
|
|||||||
Reference in New Issue
Block a user