feat: add agent publishing foundation
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::ids::{AgentId, OperationId, WorkspaceId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentStatus {
|
||||
Draft,
|
||||
Published,
|
||||
Archived,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Agent {
|
||||
pub id: AgentId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub slug: String,
|
||||
pub display_name: String,
|
||||
pub description: String,
|
||||
pub status: AgentStatus,
|
||||
pub current_draft_version: u32,
|
||||
pub latest_published_version: Option<u32>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AgentVersion {
|
||||
pub agent_id: AgentId,
|
||||
pub version: u32,
|
||||
pub status: AgentStatus,
|
||||
pub instructions: Value,
|
||||
pub tool_selection_policy: Value,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AgentOperationBinding {
|
||||
pub operation_id: OperationId,
|
||||
pub operation_version: u32,
|
||||
pub tool_name: String,
|
||||
pub tool_title: String,
|
||||
pub tool_description_override: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
@@ -42,3 +42,4 @@ define_id!(SampleId);
|
||||
define_id!(AuthProfileId);
|
||||
define_id!(WorkspaceId);
|
||||
define_id!(UserId);
|
||||
define_id!(AgentId);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod ids;
|
||||
pub mod operation;
|
||||
pub mod protocol;
|
||||
pub mod workspace;
|
||||
|
||||
pub use agent::{Agent, AgentOperationBinding, AgentStatus, AgentVersion};
|
||||
pub use auth::{
|
||||
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
|
||||
BearerAuthConfig, SecretRef,
|
||||
};
|
||||
pub use ids::{AuthProfileId, DescriptorId, OperationId, SampleId, ToolId, UserId, WorkspaceId};
|
||||
pub use ids::{
|
||||
AgentId, AuthProfileId, DescriptorId, OperationId, SampleId, ToolId, UserId, WorkspaceId,
|
||||
};
|
||||
pub use operation::{
|
||||
ConfigExport, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus, GraphqlTarget,
|
||||
GrpcProtocolOptions, GrpcTarget, Operation, OperationStatus, ProtocolOptions, RestTarget,
|
||||
|
||||
@@ -10,6 +10,13 @@ pub enum RegistryError {
|
||||
WorkspaceNotFound { workspace_id: String },
|
||||
#[error("workspace with slug {slug} already exists")]
|
||||
WorkspaceSlugAlreadyExists { slug: String },
|
||||
#[error("agent {agent_id} was not found")]
|
||||
AgentNotFound { agent_id: String },
|
||||
#[error("published agent {workspace_slug}/{agent_slug} was not found")]
|
||||
PublishedAgentNotFound {
|
||||
workspace_slug: String,
|
||||
agent_slug: String,
|
||||
},
|
||||
#[error("operation {operation_id} already exists")]
|
||||
OperationAlreadyExists { operation_id: String },
|
||||
#[error("operation {operation_id} was not found")]
|
||||
|
||||
@@ -5,10 +5,12 @@ mod postgres;
|
||||
|
||||
pub use error::RegistryError;
|
||||
pub use model::{
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
|
||||
DescriptorMetadata, OperationSampleMetadata, OperationSummary, OperationVersionRecord,
|
||||
PublishRequest, RegistryOperation, SampleKind, SaveAuthProfileRequest,
|
||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, UpdateWorkspaceRequest,
|
||||
WorkspaceRecord, YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
AgentSummary, AgentVersionRecord, CreateAgentRequest, CreateVersionRequest,
|
||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata,
|
||||
OperationSampleMetadata, OperationSummary, OperationVersionRecord, PublishAgentRequest,
|
||||
PublishRequest, PublishedAgentTool, RegistryOperation, SampleKind, SaveAgentBindingsRequest,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJob, YamlImportJobCompletion,
|
||||
YamlImportJobId, YamlImportJobStatus,
|
||||
};
|
||||
pub use postgres::PostgresRegistry;
|
||||
|
||||
@@ -191,5 +191,78 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists agents (
|
||||
id text primary key,
|
||||
workspace_id text not null references workspaces(id) on delete cascade,
|
||||
slug text not null,
|
||||
display_name text not null,
|
||||
description text not null,
|
||||
status text not null,
|
||||
current_draft_version integer not null default 1,
|
||||
latest_published_version integer null,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null,
|
||||
published_at timestamptz null
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create unique index if not exists agents_workspace_slug_idx on agents(workspace_id, slug)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists agent_versions (
|
||||
agent_id text not null references agents(id) on delete cascade,
|
||||
version integer not null,
|
||||
status text not null,
|
||||
instructions_json jsonb not null,
|
||||
tool_selection_policy_json jsonb not null,
|
||||
created_at timestamptz not null,
|
||||
primary key (agent_id, version)
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists agent_operation_bindings (
|
||||
agent_id text not null references agents(id) on delete cascade,
|
||||
agent_version integer not null,
|
||||
operation_id text not null references operations(id) on delete cascade,
|
||||
operation_version integer not null,
|
||||
tool_name text not null,
|
||||
tool_title text not null,
|
||||
tool_description_override text null,
|
||||
enabled boolean not null default true,
|
||||
foreign key (agent_id, agent_version) references agent_versions(agent_id, version) on delete cascade,
|
||||
foreign key (operation_id, operation_version) references operation_versions(operation_id, version) on delete cascade
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create unique index if not exists agent_bindings_tool_name_idx on agent_operation_bindings(agent_id, agent_version, tool_name)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists published_agents (
|
||||
agent_id text primary key references agents(id) on delete cascade,
|
||||
version integer not null,
|
||||
published_at timestamptz not null,
|
||||
published_by text null,
|
||||
foreign key (agent_id, version) references agent_versions(agent_id, version) on delete cascade
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crank_core::{
|
||||
AuthProfile, DescriptorId, ExportMode, Operation, OperationId, OperationStatus, Protocol,
|
||||
SampleId, Workspace, WorkspaceId,
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, DescriptorId,
|
||||
ExportMode, Operation, OperationId, OperationStatus, Protocol, SampleId, Workspace,
|
||||
WorkspaceId,
|
||||
};
|
||||
use crank_mapping::MappingSet;
|
||||
use crank_schema::Schema;
|
||||
@@ -45,6 +46,44 @@ pub struct WorkspaceRecord {
|
||||
pub workspace: Workspace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentSummary {
|
||||
pub id: AgentId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub slug: String,
|
||||
pub display_name: String,
|
||||
pub description: String,
|
||||
pub status: AgentStatus,
|
||||
pub current_draft_version: u32,
|
||||
pub latest_published_version: Option<u32>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentVersionRecord {
|
||||
pub agent_id: AgentId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub version: u32,
|
||||
pub status: AgentStatus,
|
||||
pub created_at: String,
|
||||
pub snapshot: AgentVersion,
|
||||
pub bindings: Vec<AgentOperationBinding>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PublishedAgentTool {
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub workspace_slug: String,
|
||||
pub agent_id: AgentId,
|
||||
pub agent_slug: String,
|
||||
pub operation: RegistryOperation,
|
||||
pub tool_name: String,
|
||||
pub tool_title: String,
|
||||
pub tool_description: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OperationSummary {
|
||||
pub id: OperationId,
|
||||
@@ -185,6 +224,30 @@ pub struct UpdateWorkspaceRequest<'a> {
|
||||
pub workspace: &'a Workspace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CreateAgentRequest<'a> {
|
||||
pub agent: &'a Agent,
|
||||
pub version: &'a AgentVersion,
|
||||
pub bindings: &'a [AgentOperationBinding],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SaveAgentBindingsRequest<'a> {
|
||||
pub workspace_id: &'a WorkspaceId,
|
||||
pub agent_id: &'a AgentId,
|
||||
pub agent_version: u32,
|
||||
pub bindings: &'a [AgentOperationBinding],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublishAgentRequest<'a> {
|
||||
pub workspace_id: &'a WorkspaceId,
|
||||
pub agent_id: &'a AgentId,
|
||||
pub version: u32,
|
||||
pub published_at: &'a str,
|
||||
pub published_by: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SaveSampleMetadataRequest<'a> {
|
||||
pub sample: &'a OperationSampleMetadata,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crank_core::{AuthProfile, OperationId, OperationStatus, Workspace, WorkspaceId};
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, OperationId,
|
||||
OperationStatus, Workspace, WorkspaceId,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
use sqlx::{
|
||||
@@ -11,11 +14,13 @@ use crate::{
|
||||
error::RegistryError,
|
||||
migrations,
|
||||
model::{
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
|
||||
DescriptorMetadata, OperationSampleMetadata, OperationSummary, OperationVersionRecord,
|
||||
PublishRequest, RegistryOperation, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJob,
|
||||
YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
AgentSummary, AgentVersionRecord, CreateAgentRequest, CreateVersionRequest,
|
||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorMetadata,
|
||||
OperationSampleMetadata, OperationSummary, OperationVersionRecord, PublishAgentRequest,
|
||||
PublishRequest, PublishedAgentTool, RegistryOperation, SaveAgentBindingsRequest,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJob, YamlImportJobCompletion,
|
||||
YamlImportJobId, YamlImportJobStatus,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -158,6 +163,312 @@ impl PostgresRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_agents(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<AgentSummary>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
slug,
|
||||
display_name,
|
||||
description,
|
||||
status,
|
||||
current_draft_version,
|
||||
latest_published_version,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
|
||||
from agents
|
||||
where workspace_id = $1
|
||||
order by slug asc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
rows.iter().map(map_agent_summary).collect()
|
||||
}
|
||||
|
||||
pub async fn get_agent_summary(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
) -> Result<Option<AgentSummary>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
slug,
|
||||
display_name,
|
||||
description,
|
||||
status,
|
||||
current_draft_version,
|
||||
latest_published_version,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
|
||||
from agents
|
||||
where workspace_id = $1 and id = $2",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(agent_id.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
row.as_ref().map(map_agent_summary).transpose()
|
||||
}
|
||||
|
||||
pub async fn create_agent(&self, request: CreateAgentRequest<'_>) -> Result<(), RegistryError> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
sqlx::query(
|
||||
"insert into agents (
|
||||
id,
|
||||
workspace_id,
|
||||
slug,
|
||||
display_name,
|
||||
description,
|
||||
status,
|
||||
current_draft_version,
|
||||
latest_published_version,
|
||||
created_at,
|
||||
updated_at,
|
||||
published_at
|
||||
) values (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9::timestamptz, $10::timestamptz, $11::timestamptz
|
||||
)",
|
||||
)
|
||||
.bind(request.agent.id.as_str())
|
||||
.bind(request.agent.workspace_id.as_str())
|
||||
.bind(&request.agent.slug)
|
||||
.bind(&request.agent.display_name)
|
||||
.bind(&request.agent.description)
|
||||
.bind(serialize_enum_text(&request.agent.status, "status")?)
|
||||
.bind(to_db_version(request.agent.current_draft_version))
|
||||
.bind(request.agent.latest_published_version.map(to_db_version))
|
||||
.bind(&request.agent.created_at)
|
||||
.bind(&request.agent.updated_at)
|
||||
.bind(request.agent.published_at.as_deref())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
insert_agent_version_row(&mut tx, request.version).await?;
|
||||
replace_agent_bindings_rows(
|
||||
&mut tx,
|
||||
&request.agent.id,
|
||||
request.version.version,
|
||||
request.bindings,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_agent_version(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
version: u32,
|
||||
) -> Result<Option<AgentVersionRecord>, RegistryError> {
|
||||
let summary = match self.get_agent_summary(workspace_id, agent_id).await? {
|
||||
Some(value) => value,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
agent_id,
|
||||
version,
|
||||
status,
|
||||
instructions_json,
|
||||
tool_selection_policy_json,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at
|
||||
from agent_versions
|
||||
where agent_id = $1 and version = $2",
|
||||
)
|
||||
.bind(agent_id.as_str())
|
||||
.bind(to_db_version(version))
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
let Some(row) = row else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let bindings = self.list_agent_bindings(agent_id, version).await?;
|
||||
Ok(Some(map_agent_version_record(&summary, &row, bindings)?))
|
||||
}
|
||||
|
||||
pub async fn save_agent_bindings(
|
||||
&self,
|
||||
request: SaveAgentBindingsRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
if self
|
||||
.get_agent_summary(request.workspace_id, request.agent_id)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Err(RegistryError::AgentNotFound {
|
||||
agent_id: request.agent_id.as_str().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut tx = self.pool.begin().await?;
|
||||
replace_agent_bindings_rows(
|
||||
&mut tx,
|
||||
request.agent_id,
|
||||
request.agent_version,
|
||||
request.bindings,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn publish_agent(
|
||||
&self,
|
||||
request: PublishAgentRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
if self
|
||||
.get_agent_version(request.workspace_id, request.agent_id, request.version)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
return Err(RegistryError::AgentNotFound {
|
||||
agent_id: request.agent_id.as_str().to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
sqlx::query(
|
||||
"insert into published_agents (
|
||||
agent_id,
|
||||
version,
|
||||
published_at,
|
||||
published_by
|
||||
) values ($1, $2, $3::timestamptz, $4)
|
||||
on conflict(agent_id) do update set
|
||||
version = excluded.version,
|
||||
published_at = excluded.published_at,
|
||||
published_by = excluded.published_by",
|
||||
)
|
||||
.bind(request.agent_id.as_str())
|
||||
.bind(to_db_version(request.version))
|
||||
.bind(request.published_at)
|
||||
.bind(request.published_by)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"update agent_versions
|
||||
set status = $1
|
||||
where agent_id = $2 and version = $3",
|
||||
)
|
||||
.bind(serialize_enum_text(&AgentStatus::Published, "status")?)
|
||||
.bind(request.agent_id.as_str())
|
||||
.bind(to_db_version(request.version))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"update agents
|
||||
set status = $1,
|
||||
latest_published_version = $2,
|
||||
published_at = $3::timestamptz,
|
||||
updated_at = $4::timestamptz
|
||||
where id = $5 and workspace_id = $6",
|
||||
)
|
||||
.bind(serialize_enum_text(&AgentStatus::Published, "status")?)
|
||||
.bind(to_db_version(request.version))
|
||||
.bind(request.published_at)
|
||||
.bind(request.published_at)
|
||||
.bind(request.agent_id.as_str())
|
||||
.bind(request.workspace_id.as_str())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_published_agent_tools_by_slug(
|
||||
&self,
|
||||
workspace_slug: &str,
|
||||
agent_slug: &str,
|
||||
) -> Result<Vec<PublishedAgentTool>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
w.id as workspace_id,
|
||||
w.slug as workspace_slug,
|
||||
a.id as agent_id,
|
||||
a.slug as agent_slug,
|
||||
b.tool_name,
|
||||
b.tool_title,
|
||||
coalesce(b.tool_description_override, ov.tool_description_json->>'description') as tool_description,
|
||||
o.id,
|
||||
o.name,
|
||||
o.display_name,
|
||||
o.protocol,
|
||||
to_char(o.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_created_at,
|
||||
to_char(o.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_updated_at,
|
||||
to_char(o.published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as operation_published_at,
|
||||
ov.version,
|
||||
ov.status,
|
||||
ov.target_json,
|
||||
ov.input_schema_json,
|
||||
ov.output_schema_json,
|
||||
ov.input_mapping_json,
|
||||
ov.output_mapping_json,
|
||||
ov.execution_config_json,
|
||||
ov.tool_description_json,
|
||||
ov.samples_json,
|
||||
ov.generated_draft_json,
|
||||
ov.config_export_json,
|
||||
ov.change_note,
|
||||
to_char(ov.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
ov.created_by
|
||||
from workspaces w
|
||||
join agents a on a.workspace_id = w.id
|
||||
join published_agents pa on pa.agent_id = a.id
|
||||
join agent_operation_bindings b on b.agent_id = a.id and b.agent_version = pa.version
|
||||
join operation_versions ov on ov.operation_id = b.operation_id and ov.version = b.operation_version
|
||||
join operations o on o.id = ov.operation_id and o.workspace_id = w.id
|
||||
where w.slug = $1 and a.slug = $2 and b.enabled = true
|
||||
order by b.tool_name asc",
|
||||
)
|
||||
.bind(workspace_slug)
|
||||
.bind(agent_slug)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
if rows.is_empty() {
|
||||
let exists = sqlx::query(
|
||||
"select 1
|
||||
from workspaces w
|
||||
join agents a on a.workspace_id = w.id
|
||||
join published_agents pa on pa.agent_id = a.id
|
||||
where w.slug = $1 and a.slug = $2",
|
||||
)
|
||||
.bind(workspace_slug)
|
||||
.bind(agent_slug)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
if exists.is_none() {
|
||||
return Err(RegistryError::PublishedAgentNotFound {
|
||||
workspace_slug: workspace_slug.to_owned(),
|
||||
agent_slug: agent_slug.to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rows.iter().map(map_published_agent_tool).collect()
|
||||
}
|
||||
|
||||
async fn connect_in_schema(
|
||||
database_url: &str,
|
||||
schema: Option<&str>,
|
||||
@@ -915,6 +1226,31 @@ impl PostgresRegistry {
|
||||
|
||||
row.as_ref().map(map_yaml_import_job).transpose()
|
||||
}
|
||||
|
||||
async fn list_agent_bindings(
|
||||
&self,
|
||||
agent_id: &AgentId,
|
||||
version: u32,
|
||||
) -> Result<Vec<AgentOperationBinding>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
operation_id,
|
||||
operation_version,
|
||||
tool_name,
|
||||
tool_title,
|
||||
tool_description_override,
|
||||
enabled
|
||||
from agent_operation_bindings
|
||||
where agent_id = $1 and agent_version = $2
|
||||
order by tool_name asc",
|
||||
)
|
||||
.bind(agent_id.as_str())
|
||||
.bind(to_db_version(version))
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
rows.iter().map(map_agent_binding).collect()
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert_version_row(
|
||||
@@ -968,6 +1304,77 @@ async fn insert_version_row(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_agent_version_row(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
version: &AgentVersion,
|
||||
) -> Result<(), RegistryError> {
|
||||
sqlx::query(
|
||||
"insert into agent_versions (
|
||||
agent_id,
|
||||
version,
|
||||
status,
|
||||
instructions_json,
|
||||
tool_selection_policy_json,
|
||||
created_at
|
||||
) values (
|
||||
$1, $2, $3, $4, $5, $6::timestamptz
|
||||
)",
|
||||
)
|
||||
.bind(version.agent_id.as_str())
|
||||
.bind(to_db_version(version.version))
|
||||
.bind(serialize_enum_text(&version.status, "status")?)
|
||||
.bind(Json(version.instructions.clone()))
|
||||
.bind(Json(version.tool_selection_policy.clone()))
|
||||
.bind(&version.created_at)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn replace_agent_bindings_rows(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
agent_id: &AgentId,
|
||||
version: u32,
|
||||
bindings: &[AgentOperationBinding],
|
||||
) -> Result<(), RegistryError> {
|
||||
sqlx::query(
|
||||
"delete from agent_operation_bindings
|
||||
where agent_id = $1 and agent_version = $2",
|
||||
)
|
||||
.bind(agent_id.as_str())
|
||||
.bind(to_db_version(version))
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
for binding in bindings {
|
||||
sqlx::query(
|
||||
"insert into agent_operation_bindings (
|
||||
agent_id,
|
||||
agent_version,
|
||||
operation_id,
|
||||
operation_version,
|
||||
tool_name,
|
||||
tool_title,
|
||||
tool_description_override,
|
||||
enabled
|
||||
) values ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
)
|
||||
.bind(agent_id.as_str())
|
||||
.bind(to_db_version(version))
|
||||
.bind(binding.operation_id.as_str())
|
||||
.bind(to_db_version(binding.operation_version))
|
||||
.bind(&binding.tool_name)
|
||||
.bind(&binding.tool_title)
|
||||
.bind(binding.tool_description_override.as_deref())
|
||||
.bind(binding.enabled)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn assert_immutable_fields(
|
||||
summary: &OperationSummary,
|
||||
snapshot: &RegistryOperation,
|
||||
@@ -1010,6 +1417,28 @@ fn map_workspace_record(row: &PgRow) -> Result<WorkspaceRecord, RegistryError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn map_agent_summary(row: &PgRow) -> Result<AgentSummary, RegistryError> {
|
||||
Ok(AgentSummary {
|
||||
id: AgentId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
slug: row.try_get("slug")?,
|
||||
display_name: row.try_get("display_name")?,
|
||||
description: row.try_get("description")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
current_draft_version: from_db_version(
|
||||
row.try_get("current_draft_version")?,
|
||||
"current_draft_version",
|
||||
)?,
|
||||
latest_published_version: row
|
||||
.try_get::<Option<i32>, _>("latest_published_version")?
|
||||
.map(|value| from_db_version(value, "latest_published_version"))
|
||||
.transpose()?,
|
||||
created_at: row.try_get("created_at")?,
|
||||
updated_at: row.try_get("updated_at")?,
|
||||
published_at: row.try_get("published_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_operation_summary(row: &PgRow) -> Result<OperationSummary, RegistryError> {
|
||||
Ok(OperationSummary {
|
||||
id: OperationId::new(row.try_get::<String, _>("id")?),
|
||||
@@ -1102,6 +1531,60 @@ fn map_auth_profile(row: &PgRow) -> Result<AuthProfile, RegistryError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn map_agent_binding(row: &PgRow) -> Result<AgentOperationBinding, RegistryError> {
|
||||
Ok(AgentOperationBinding {
|
||||
operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
|
||||
operation_version: from_db_version(row.try_get("operation_version")?, "operation_version")?,
|
||||
tool_name: row.try_get("tool_name")?,
|
||||
tool_title: row.try_get("tool_title")?,
|
||||
tool_description_override: row.try_get("tool_description_override")?,
|
||||
enabled: row.try_get("enabled")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_agent_version_record(
|
||||
summary: &AgentSummary,
|
||||
row: &PgRow,
|
||||
bindings: Vec<AgentOperationBinding>,
|
||||
) -> Result<AgentVersionRecord, RegistryError> {
|
||||
let version = from_db_version(row.try_get("version")?, "version")?;
|
||||
let status = deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?;
|
||||
|
||||
Ok(AgentVersionRecord {
|
||||
agent_id: summary.id.clone(),
|
||||
workspace_id: summary.workspace_id.clone(),
|
||||
version,
|
||||
status,
|
||||
created_at: row.try_get("created_at")?,
|
||||
bindings,
|
||||
snapshot: AgentVersion {
|
||||
agent_id: summary.id.clone(),
|
||||
version,
|
||||
status,
|
||||
instructions: row.try_get::<Json<Value>, _>("instructions_json")?.0,
|
||||
tool_selection_policy: row
|
||||
.try_get::<Json<Value>, _>("tool_selection_policy_json")?
|
||||
.0,
|
||||
created_at: row.try_get("created_at")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_published_agent_tool(row: &PgRow) -> Result<PublishedAgentTool, RegistryError> {
|
||||
let record = map_operation_version_record(row)?;
|
||||
|
||||
Ok(PublishedAgentTool {
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
workspace_slug: row.try_get("workspace_slug")?,
|
||||
agent_id: AgentId::new(row.try_get::<String, _>("agent_id")?),
|
||||
agent_slug: row.try_get("agent_slug")?,
|
||||
tool_name: row.try_get("tool_name")?,
|
||||
tool_title: row.try_get("tool_title")?,
|
||||
tool_description: row.try_get("tool_description")?,
|
||||
operation: record.snapshot,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_sample_metadata(row: &PgRow) -> Result<OperationSampleMetadata, RegistryError> {
|
||||
Ok(OperationSampleMetadata {
|
||||
id: crank_core::SampleId::new(row.try_get::<String, _>("id")?),
|
||||
|
||||
Reference in New Issue
Block a user