feat: subagent table structure and crud apis

This commit is contained in:
Acbox
2026-01-31 19:58:17 +08:00
parent 9fd15bfa6b
commit fe50b1d224
13 changed files with 2689 additions and 2 deletions
+4 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/memohai/memoh/internal/schedule"
"github.com/memohai/memoh/internal/settings"
"github.com/memohai/memoh/internal/server"
"github.com/memohai/memoh/internal/subagent"
"github.com/jackc/pgx/v5/pgtype"
"golang.org/x/crypto/bcrypt"
@@ -168,7 +169,9 @@ func main() {
log.Fatalf("schedule bootstrap: %v", err)
}
scheduleHandler := handlers.NewScheduleHandler(scheduleService)
srv := server.NewServer(addr, cfg.Auth.JWTSecret, pingHandler, authHandler, memoryHandler, embeddingsHandler, chatHandler, swaggerHandler, providersHandler, modelsHandler, settingsHandler, historyHandler, scheduleHandler, containerdHandler)
subagentService := subagent.NewService(queries)
subagentHandler := handlers.NewSubagentHandler(subagentService)
srv := server.NewServer(addr, cfg.Auth.JWTSecret, pingHandler, authHandler, memoryHandler, embeddingsHandler, chatHandler, swaggerHandler, providersHandler, modelsHandler, settingsHandler, historyHandler, scheduleHandler, subagentHandler, containerdHandler)
if err := srv.Start(); err != nil {
log.Fatalf("server failed: %v", err)
+1
View File
@@ -1,6 +1,7 @@
DROP TABLE IF EXISTS user_settings;
DROP TABLE IF EXISTS history;
DROP TABLE IF EXISTS schedule;
DROP TABLE IF EXISTS subagents;
DROP TABLE IF EXISTS lifecycle_events;
DROP TABLE IF EXISTS container_versions;
DROP TABLE IF EXISTS models;
+18
View File
@@ -163,3 +163,21 @@ CREATE TABLE IF NOT EXISTS schedule (
CREATE INDEX IF NOT EXISTS idx_schedule_user_id ON schedule(user_id);
CREATE INDEX IF NOT EXISTS idx_schedule_enabled ON schedule(enabled);
CREATE TABLE IF NOT EXISTS subagents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted BOOLEAN NOT NULL DEFAULT false,
deleted_at TIMESTAMPTZ,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
messages JSONB NOT NULL DEFAULT '[]'::jsonb,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
skills JSONB NOT NULL DEFAULT '[]'::jsonb,
CONSTRAINT subagents_name_unique UNIQUE (name)
);
CREATE INDEX IF NOT EXISTS idx_subagents_user_id ON subagents(user_id);
CREATE INDEX IF NOT EXISTS idx_subagents_deleted ON subagents(deleted);
+46
View File
@@ -0,0 +1,46 @@
-- name: CreateSubagent :one
INSERT INTO subagents (name, description, user_id, messages, metadata, skills)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills;
-- name: GetSubagentByID :one
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills
FROM subagents
WHERE id = $1 AND deleted = false;
-- name: ListSubagentsByUser :many
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills
FROM subagents
WHERE user_id = $1 AND deleted = false
ORDER BY created_at DESC;
-- name: UpdateSubagent :one
UPDATE subagents
SET name = $2,
description = $3,
metadata = $4,
updated_at = now()
WHERE id = $1 AND deleted = false
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills;
-- name: UpdateSubagentMessages :one
UPDATE subagents
SET messages = $2,
updated_at = now()
WHERE id = $1 AND deleted = false
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills;
-- name: UpdateSubagentSkills :one
UPDATE subagents
SET skills = $2,
updated_at = now()
WHERE id = $1 AND deleted = false
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills;
-- name: SoftDeleteSubagent :exec
UPDATE subagents
SET deleted = true,
deleted_at = now(),
updated_at = now()
WHERE id = $1 AND deleted = false;
+599
View File
@@ -1921,6 +1921,448 @@ const docTemplate = `{
}
}
}
},
"/subagents": {
"get": {
"description": "List subagents for current user",
"tags": [
"subagent"
],
"summary": "List subagents",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.ListResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a subagent for current user",
"tags": [
"subagent"
],
"summary": "Create subagent",
"parameters": [
{
"description": "Subagent payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.CreateRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/subagent.Subagent"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/subagents/{id}": {
"get": {
"description": "Get a subagent by ID",
"tags": [
"subagent"
],
"summary": "Get subagent",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.Subagent"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Update a subagent by ID",
"tags": [
"subagent"
],
"summary": "Update subagent",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Subagent payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.UpdateRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.Subagent"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete a subagent by ID",
"tags": [
"subagent"
],
"summary": "Delete subagent",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/subagents/{id}/context": {
"get": {
"description": "Get a subagent's message context",
"tags": [
"subagent"
],
"summary": "Get subagent context",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.ContextResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Update a subagent's message context",
"tags": [
"subagent"
],
"summary": "Update subagent context",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Context payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.UpdateContextRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.ContextResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/subagents/{id}/skills": {
"get": {
"description": "Get a subagent's skills",
"tags": [
"subagent"
],
"summary": "Get subagent skills",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.SkillsResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Replace a subagent's skills",
"tags": [
"subagent"
],
"summary": "Update subagent skills",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Skills payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.UpdateSkillsRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.SkillsResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"description": "Add skills to a subagent",
"tags": [
"subagent"
],
"summary": "Add subagent skills",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Skills payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.AddSkillsRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.SkillsResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
}
},
"definitions": {
@@ -2775,6 +3217,163 @@ const docTemplate = `{
"type": "integer"
}
}
},
"subagent.AddSkillsRequest": {
"type": "object",
"properties": {
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"subagent.ContextResponse": {
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
},
"subagent.CreateRequest": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"metadata": {
"type": "object",
"additionalProperties": true
},
"name": {
"type": "string"
},
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"subagent.ListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/subagent.Subagent"
}
}
}
},
"subagent.SkillsResponse": {
"type": "object",
"properties": {
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"subagent.Subagent": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"deleted": {
"type": "boolean"
},
"deleted_at": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"metadata": {
"type": "object",
"additionalProperties": true
},
"name": {
"type": "string"
},
"skills": {
"type": "array",
"items": {
"type": "string"
}
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"subagent.UpdateContextRequest": {
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
},
"subagent.UpdateRequest": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": true
},
"name": {
"type": "string"
}
}
},
"subagent.UpdateSkillsRequest": {
"type": "object",
"properties": {
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}`
+599
View File
@@ -1912,6 +1912,448 @@
}
}
}
},
"/subagents": {
"get": {
"description": "List subagents for current user",
"tags": [
"subagent"
],
"summary": "List subagents",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.ListResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a subagent for current user",
"tags": [
"subagent"
],
"summary": "Create subagent",
"parameters": [
{
"description": "Subagent payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.CreateRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/subagent.Subagent"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/subagents/{id}": {
"get": {
"description": "Get a subagent by ID",
"tags": [
"subagent"
],
"summary": "Get subagent",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.Subagent"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Update a subagent by ID",
"tags": [
"subagent"
],
"summary": "Update subagent",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Subagent payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.UpdateRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.Subagent"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete a subagent by ID",
"tags": [
"subagent"
],
"summary": "Delete subagent",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/subagents/{id}/context": {
"get": {
"description": "Get a subagent's message context",
"tags": [
"subagent"
],
"summary": "Get subagent context",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.ContextResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Update a subagent's message context",
"tags": [
"subagent"
],
"summary": "Update subagent context",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Context payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.UpdateContextRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.ContextResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/subagents/{id}/skills": {
"get": {
"description": "Get a subagent's skills",
"tags": [
"subagent"
],
"summary": "Get subagent skills",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.SkillsResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Replace a subagent's skills",
"tags": [
"subagent"
],
"summary": "Update subagent skills",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Skills payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.UpdateSkillsRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.SkillsResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"description": "Add skills to a subagent",
"tags": [
"subagent"
],
"summary": "Add subagent skills",
"parameters": [
{
"type": "string",
"description": "Subagent ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Skills payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/subagent.AddSkillsRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/subagent.SkillsResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
}
},
"definitions": {
@@ -2766,6 +3208,163 @@
"type": "integer"
}
}
},
"subagent.AddSkillsRequest": {
"type": "object",
"properties": {
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"subagent.ContextResponse": {
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
},
"subagent.CreateRequest": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"metadata": {
"type": "object",
"additionalProperties": true
},
"name": {
"type": "string"
},
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"subagent.ListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/subagent.Subagent"
}
}
}
},
"subagent.SkillsResponse": {
"type": "object",
"properties": {
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"subagent.Subagent": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"deleted": {
"type": "boolean"
},
"deleted_at": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"metadata": {
"type": "object",
"additionalProperties": true
},
"name": {
"type": "string"
},
"skills": {
"type": "array",
"items": {
"type": "string"
}
},
"updated_at": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"subagent.UpdateContextRequest": {
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
},
"subagent.UpdateRequest": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": true
},
"name": {
"type": "string"
}
}
},
"subagent.UpdateSkillsRequest": {
"type": "object",
"properties": {
"skills": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
+397
View File
@@ -563,6 +563,110 @@ definitions:
max_context_load_time:
type: integer
type: object
subagent.AddSkillsRequest:
properties:
skills:
items:
type: string
type: array
type: object
subagent.ContextResponse:
properties:
messages:
items:
additionalProperties: true
type: object
type: array
type: object
subagent.CreateRequest:
properties:
description:
type: string
messages:
items:
additionalProperties: true
type: object
type: array
metadata:
additionalProperties: true
type: object
name:
type: string
skills:
items:
type: string
type: array
type: object
subagent.ListResponse:
properties:
items:
items:
$ref: '#/definitions/subagent.Subagent'
type: array
type: object
subagent.SkillsResponse:
properties:
skills:
items:
type: string
type: array
type: object
subagent.Subagent:
properties:
created_at:
type: string
deleted:
type: boolean
deleted_at:
type: string
description:
type: string
id:
type: string
messages:
items:
additionalProperties: true
type: object
type: array
metadata:
additionalProperties: true
type: object
name:
type: string
skills:
items:
type: string
type: array
updated_at:
type: string
user_id:
type: string
type: object
subagent.UpdateContextRequest:
properties:
messages:
items:
additionalProperties: true
type: object
type: array
type: object
subagent.UpdateRequest:
properties:
description:
type: string
metadata:
additionalProperties: true
type: object
name:
type: string
type: object
subagent.UpdateSkillsRequest:
properties:
skills:
items:
type: string
type: array
type: object
info:
contact: {}
title: Memoh API
@@ -1828,4 +1932,297 @@ paths:
summary: Update user settings
tags:
- settings
/subagents:
get:
description: List subagents for current user
responses:
"200":
description: OK
schema:
$ref: '#/definitions/subagent.ListResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: List subagents
tags:
- subagent
post:
description: Create a subagent for current user
parameters:
- description: Subagent payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/subagent.CreateRequest'
responses:
"201":
description: Created
schema:
$ref: '#/definitions/subagent.Subagent'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Create subagent
tags:
- subagent
/subagents/{id}:
delete:
description: Delete a subagent by ID
parameters:
- description: Subagent ID
in: path
name: id
required: true
type: string
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Delete subagent
tags:
- subagent
get:
description: Get a subagent by ID
parameters:
- description: Subagent ID
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/subagent.Subagent'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Get subagent
tags:
- subagent
put:
description: Update a subagent by ID
parameters:
- description: Subagent ID
in: path
name: id
required: true
type: string
- description: Subagent payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/subagent.UpdateRequest'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/subagent.Subagent'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Update subagent
tags:
- subagent
/subagents/{id}/context:
get:
description: Get a subagent's message context
parameters:
- description: Subagent ID
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/subagent.ContextResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Get subagent context
tags:
- subagent
put:
description: Update a subagent's message context
parameters:
- description: Subagent ID
in: path
name: id
required: true
type: string
- description: Context payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/subagent.UpdateContextRequest'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/subagent.ContextResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Update subagent context
tags:
- subagent
/subagents/{id}/skills:
get:
description: Get a subagent's skills
parameters:
- description: Subagent ID
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/subagent.SkillsResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Get subagent skills
tags:
- subagent
post:
description: Add skills to a subagent
parameters:
- description: Subagent ID
in: path
name: id
required: true
type: string
- description: Skills payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/subagent.AddSkillsRequest'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/subagent.SkillsResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Add subagent skills
tags:
- subagent
put:
description: Replace a subagent's skills
parameters:
- description: Subagent ID
in: path
name: id
required: true
type: string
- description: Skills payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/subagent.UpdateSkillsRequest'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/subagent.SkillsResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Update subagent skills
tags:
- subagent
swagger: "2.0"
+14
View File
@@ -105,6 +105,20 @@ type Snapshot struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Subagent struct {
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Deleted bool `json:"deleted"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
UserID pgtype.UUID `json:"user_id"`
Messages []byte `json:"messages"`
Metadata []byte `json:"metadata"`
Skills []byte `json:"skills"`
}
type User struct {
ID pgtype.UUID `json:"id"`
Username string `json:"username"`
+235
View File
@@ -0,0 +1,235 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: subagents.sql
package sqlc
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createSubagent = `-- name: CreateSubagent :one
INSERT INTO subagents (name, description, user_id, messages, metadata, skills)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills
`
type CreateSubagentParams struct {
Name string `json:"name"`
Description string `json:"description"`
UserID pgtype.UUID `json:"user_id"`
Messages []byte `json:"messages"`
Metadata []byte `json:"metadata"`
Skills []byte `json:"skills"`
}
func (q *Queries) CreateSubagent(ctx context.Context, arg CreateSubagentParams) (Subagent, error) {
row := q.db.QueryRow(ctx, createSubagent,
arg.Name,
arg.Description,
arg.UserID,
arg.Messages,
arg.Metadata,
arg.Skills,
)
var i Subagent
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.DeletedAt,
&i.UserID,
&i.Messages,
&i.Metadata,
&i.Skills,
)
return i, err
}
const getSubagentByID = `-- name: GetSubagentByID :one
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills
FROM subagents
WHERE id = $1 AND deleted = false
`
func (q *Queries) GetSubagentByID(ctx context.Context, id pgtype.UUID) (Subagent, error) {
row := q.db.QueryRow(ctx, getSubagentByID, id)
var i Subagent
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.DeletedAt,
&i.UserID,
&i.Messages,
&i.Metadata,
&i.Skills,
)
return i, err
}
const listSubagentsByUser = `-- name: ListSubagentsByUser :many
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills
FROM subagents
WHERE user_id = $1 AND deleted = false
ORDER BY created_at DESC
`
func (q *Queries) ListSubagentsByUser(ctx context.Context, userID pgtype.UUID) ([]Subagent, error) {
rows, err := q.db.Query(ctx, listSubagentsByUser, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Subagent
for rows.Next() {
var i Subagent
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.DeletedAt,
&i.UserID,
&i.Messages,
&i.Metadata,
&i.Skills,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const softDeleteSubagent = `-- name: SoftDeleteSubagent :exec
UPDATE subagents
SET deleted = true,
deleted_at = now(),
updated_at = now()
WHERE id = $1 AND deleted = false
`
func (q *Queries) SoftDeleteSubagent(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, softDeleteSubagent, id)
return err
}
const updateSubagent = `-- name: UpdateSubagent :one
UPDATE subagents
SET name = $2,
description = $3,
metadata = $4,
updated_at = now()
WHERE id = $1 AND deleted = false
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills
`
type UpdateSubagentParams struct {
ID pgtype.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Metadata []byte `json:"metadata"`
}
func (q *Queries) UpdateSubagent(ctx context.Context, arg UpdateSubagentParams) (Subagent, error) {
row := q.db.QueryRow(ctx, updateSubagent,
arg.ID,
arg.Name,
arg.Description,
arg.Metadata,
)
var i Subagent
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.DeletedAt,
&i.UserID,
&i.Messages,
&i.Metadata,
&i.Skills,
)
return i, err
}
const updateSubagentMessages = `-- name: UpdateSubagentMessages :one
UPDATE subagents
SET messages = $2,
updated_at = now()
WHERE id = $1 AND deleted = false
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills
`
type UpdateSubagentMessagesParams struct {
ID pgtype.UUID `json:"id"`
Messages []byte `json:"messages"`
}
func (q *Queries) UpdateSubagentMessages(ctx context.Context, arg UpdateSubagentMessagesParams) (Subagent, error) {
row := q.db.QueryRow(ctx, updateSubagentMessages, arg.ID, arg.Messages)
var i Subagent
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.DeletedAt,
&i.UserID,
&i.Messages,
&i.Metadata,
&i.Skills,
)
return i, err
}
const updateSubagentSkills = `-- name: UpdateSubagentSkills :one
UPDATE subagents
SET skills = $2,
updated_at = now()
WHERE id = $1 AND deleted = false
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, user_id, messages, metadata, skills
`
type UpdateSubagentSkillsParams struct {
ID pgtype.UUID `json:"id"`
Skills []byte `json:"skills"`
}
func (q *Queries) UpdateSubagentSkills(ctx context.Context, arg UpdateSubagentSkillsParams) (Subagent, error) {
row := q.db.QueryRow(ctx, updateSubagentSkills, arg.ID, arg.Skills)
var i Subagent
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
&i.UpdatedAt,
&i.Deleted,
&i.DeletedAt,
&i.UserID,
&i.Messages,
&i.Metadata,
&i.Skills,
)
return i, err
}
+361
View File
@@ -0,0 +1,361 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/memohai/memoh/internal/auth"
"github.com/memohai/memoh/internal/identity"
"github.com/memohai/memoh/internal/subagent"
)
type SubagentHandler struct {
service *subagent.Service
}
func NewSubagentHandler(service *subagent.Service) *SubagentHandler {
return &SubagentHandler{service: service}
}
func (h *SubagentHandler) Register(e *echo.Echo) {
group := e.Group("/subagents")
group.POST("", h.Create)
group.GET("", h.List)
group.GET("/:id", h.Get)
group.PUT("/:id", h.Update)
group.DELETE("/:id", h.Delete)
group.GET("/:id/context", h.GetContext)
group.PUT("/:id/context", h.UpdateContext)
group.GET("/:id/skills", h.GetSkills)
group.PUT("/:id/skills", h.UpdateSkills)
group.POST("/:id/skills", h.AddSkills)
}
// Create godoc
// @Summary Create subagent
// @Description Create a subagent for current user
// @Tags subagent
// @Param payload body subagent.CreateRequest true "Subagent payload"
// @Success 201 {object} subagent.Subagent
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents [post]
func (h *SubagentHandler) Create(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
var req subagent.CreateRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
resp, err := h.service.Create(c.Request().Context(), userID, req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusCreated, resp)
}
// List godoc
// @Summary List subagents
// @Description List subagents for current user
// @Tags subagent
// @Success 200 {object} subagent.ListResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents [get]
func (h *SubagentHandler) List(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
items, err := h.service.List(c.Request().Context(), userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, subagent.ListResponse{Items: items})
}
// Get godoc
// @Summary Get subagent
// @Description Get a subagent by ID
// @Tags subagent
// @Param id path string true "Subagent ID"
// @Success 200 {object} subagent.Subagent
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents/{id} [get]
func (h *SubagentHandler) Get(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
item, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if item.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
return c.JSON(http.StatusOK, item)
}
// Update godoc
// @Summary Update subagent
// @Description Update a subagent by ID
// @Tags subagent
// @Param id path string true "Subagent ID"
// @Param payload body subagent.UpdateRequest true "Subagent payload"
// @Success 200 {object} subagent.Subagent
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents/{id} [put]
func (h *SubagentHandler) Update(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
var req subagent.UpdateRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
item, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if item.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
resp, err := h.service.Update(c.Request().Context(), id, req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, resp)
}
// Delete godoc
// @Summary Delete subagent
// @Description Delete a subagent by ID
// @Tags subagent
// @Param id path string true "Subagent ID"
// @Success 204 "No Content"
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents/{id} [delete]
func (h *SubagentHandler) Delete(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
item, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if item.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
if err := h.service.Delete(c.Request().Context(), id); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.NoContent(http.StatusNoContent)
}
// GetContext godoc
// @Summary Get subagent context
// @Description Get a subagent's message context
// @Tags subagent
// @Param id path string true "Subagent ID"
// @Success 200 {object} subagent.ContextResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents/{id}/context [get]
func (h *SubagentHandler) GetContext(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
item, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if item.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
return c.JSON(http.StatusOK, subagent.ContextResponse{Messages: item.Messages})
}
// UpdateContext godoc
// @Summary Update subagent context
// @Description Update a subagent's message context
// @Tags subagent
// @Param id path string true "Subagent ID"
// @Param payload body subagent.UpdateContextRequest true "Context payload"
// @Success 200 {object} subagent.ContextResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents/{id}/context [put]
func (h *SubagentHandler) UpdateContext(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
var req subagent.UpdateContextRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
item, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if item.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
updated, err := h.service.UpdateContext(c.Request().Context(), id, req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, subagent.ContextResponse{Messages: updated.Messages})
}
// GetSkills godoc
// @Summary Get subagent skills
// @Description Get a subagent's skills
// @Tags subagent
// @Param id path string true "Subagent ID"
// @Success 200 {object} subagent.SkillsResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents/{id}/skills [get]
func (h *SubagentHandler) GetSkills(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
item, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if item.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
return c.JSON(http.StatusOK, subagent.SkillsResponse{Skills: item.Skills})
}
// UpdateSkills godoc
// @Summary Update subagent skills
// @Description Replace a subagent's skills
// @Tags subagent
// @Param id path string true "Subagent ID"
// @Param payload body subagent.UpdateSkillsRequest true "Skills payload"
// @Success 200 {object} subagent.SkillsResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents/{id}/skills [put]
func (h *SubagentHandler) UpdateSkills(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
var req subagent.UpdateSkillsRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
item, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if item.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
updated, err := h.service.UpdateSkills(c.Request().Context(), id, req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, subagent.SkillsResponse{Skills: updated.Skills})
}
// AddSkills godoc
// @Summary Add subagent skills
// @Description Add skills to a subagent
// @Tags subagent
// @Param id path string true "Subagent ID"
// @Param payload body subagent.AddSkillsRequest true "Skills payload"
// @Success 200 {object} subagent.SkillsResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /subagents/{id}/skills [post]
func (h *SubagentHandler) AddSkills(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
var req subagent.AddSkillsRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
item, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if item.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
updated, err := h.service.AddSkills(c.Request().Context(), id, req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, subagent.SkillsResponse{Skills: updated.Skills})
}
func (h *SubagentHandler) requireUserID(c echo.Context) (string, error) {
userID, err := auth.UserIDFromContext(c)
if err != nil {
return "", err
}
if err := identity.ValidateUserID(userID); err != nil {
return "", echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return userID, nil
}
+4 -1
View File
@@ -15,7 +15,7 @@ type Server struct {
addr string
}
func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler, authHandler *handlers.AuthHandler, memoryHandler *handlers.MemoryHandler, embeddingsHandler *handlers.EmbeddingsHandler, chatHandler *handlers.ChatHandler, swaggerHandler *handlers.SwaggerHandler, providersHandler *handlers.ProvidersHandler, modelsHandler *handlers.ModelsHandler, settingsHandler *handlers.SettingsHandler, historyHandler *handlers.HistoryHandler, scheduleHandler *handlers.ScheduleHandler, containerdHandler *handlers.ContainerdHandler) *Server {
func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler, authHandler *handlers.AuthHandler, memoryHandler *handlers.MemoryHandler, embeddingsHandler *handlers.EmbeddingsHandler, chatHandler *handlers.ChatHandler, swaggerHandler *handlers.SwaggerHandler, providersHandler *handlers.ProvidersHandler, modelsHandler *handlers.ModelsHandler, settingsHandler *handlers.SettingsHandler, historyHandler *handlers.HistoryHandler, scheduleHandler *handlers.ScheduleHandler, subagentHandler *handlers.SubagentHandler, containerdHandler *handlers.ContainerdHandler) *Server {
if addr == "" {
addr = ":8080"
}
@@ -65,6 +65,9 @@ func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler,
if scheduleHandler != nil {
scheduleHandler.Register(e)
}
if subagentHandler != nil {
subagentHandler.Register(e)
}
if providersHandler != nil {
providersHandler.Register(e)
}
+355
View File
@@ -0,0 +1,355 @@
package subagent
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/memohai/memoh/internal/db/sqlc"
)
type Service struct {
queries *sqlc.Queries
}
func NewService(queries *sqlc.Queries) *Service {
return &Service{queries: queries}
}
func (s *Service) Create(ctx context.Context, userID string, req CreateRequest) (Subagent, error) {
if s.queries == nil {
return Subagent{}, fmt.Errorf("subagent queries not configured")
}
name := strings.TrimSpace(req.Name)
if name == "" {
return Subagent{}, fmt.Errorf("name is required")
}
description := strings.TrimSpace(req.Description)
if description == "" {
return Subagent{}, fmt.Errorf("description is required")
}
pgUserID, err := parseUUID(userID)
if err != nil {
return Subagent{}, err
}
messagesPayload, err := marshalMessages(req.Messages)
if err != nil {
return Subagent{}, err
}
metadataPayload, err := marshalMetadata(req.Metadata)
if err != nil {
return Subagent{}, err
}
skillsPayload, err := marshalSkills(req.Skills)
if err != nil {
return Subagent{}, err
}
row, err := s.queries.CreateSubagent(ctx, sqlc.CreateSubagentParams{
Name: name,
Description: description,
UserID: pgUserID,
Messages: messagesPayload,
Metadata: metadataPayload,
Skills: skillsPayload,
})
if err != nil {
return Subagent{}, err
}
return toSubagent(row)
}
func (s *Service) Get(ctx context.Context, id string) (Subagent, error) {
pgID, err := parseUUID(id)
if err != nil {
return Subagent{}, err
}
row, err := s.queries.GetSubagentByID(ctx, pgID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return Subagent{}, fmt.Errorf("subagent not found")
}
return Subagent{}, err
}
return toSubagent(row)
}
func (s *Service) List(ctx context.Context, userID string) ([]Subagent, error) {
pgUserID, err := parseUUID(userID)
if err != nil {
return nil, err
}
rows, err := s.queries.ListSubagentsByUser(ctx, pgUserID)
if err != nil {
return nil, err
}
items := make([]Subagent, 0, len(rows))
for _, row := range rows {
item, err := toSubagent(row)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}
func (s *Service) Update(ctx context.Context, id string, req UpdateRequest) (Subagent, error) {
existing, err := s.Get(ctx, id)
if err != nil {
return Subagent{}, err
}
name := existing.Name
if req.Name != nil {
name = strings.TrimSpace(*req.Name)
if name == "" {
return Subagent{}, fmt.Errorf("name is required")
}
}
description := existing.Description
if req.Description != nil {
description = strings.TrimSpace(*req.Description)
if description == "" {
return Subagent{}, fmt.Errorf("description is required")
}
}
metadata := existing.Metadata
if req.Metadata != nil {
metadata = req.Metadata
}
metadataPayload, err := marshalMetadata(metadata)
if err != nil {
return Subagent{}, err
}
pgID, err := parseUUID(id)
if err != nil {
return Subagent{}, err
}
row, err := s.queries.UpdateSubagent(ctx, sqlc.UpdateSubagentParams{
ID: pgID,
Name: name,
Description: description,
Metadata: metadataPayload,
})
if err != nil {
return Subagent{}, err
}
return toSubagent(row)
}
func (s *Service) UpdateContext(ctx context.Context, id string, req UpdateContextRequest) (Subagent, error) {
messagesPayload, err := marshalMessages(req.Messages)
if err != nil {
return Subagent{}, err
}
pgID, err := parseUUID(id)
if err != nil {
return Subagent{}, err
}
row, err := s.queries.UpdateSubagentMessages(ctx, sqlc.UpdateSubagentMessagesParams{
ID: pgID,
Messages: messagesPayload,
})
if err != nil {
return Subagent{}, err
}
return toSubagent(row)
}
func (s *Service) UpdateSkills(ctx context.Context, id string, req UpdateSkillsRequest) (Subagent, error) {
skillsPayload, err := marshalSkills(req.Skills)
if err != nil {
return Subagent{}, err
}
pgID, err := parseUUID(id)
if err != nil {
return Subagent{}, err
}
row, err := s.queries.UpdateSubagentSkills(ctx, sqlc.UpdateSubagentSkillsParams{
ID: pgID,
Skills: skillsPayload,
})
if err != nil {
return Subagent{}, err
}
return toSubagent(row)
}
func (s *Service) AddSkills(ctx context.Context, id string, req AddSkillsRequest) (Subagent, error) {
existing, err := s.Get(ctx, id)
if err != nil {
return Subagent{}, err
}
merged := mergeSkills(existing.Skills, req.Skills)
payload, err := marshalSkills(merged)
if err != nil {
return Subagent{}, err
}
pgID, err := parseUUID(id)
if err != nil {
return Subagent{}, err
}
row, err := s.queries.UpdateSubagentSkills(ctx, sqlc.UpdateSubagentSkillsParams{
ID: pgID,
Skills: payload,
})
if err != nil {
return Subagent{}, err
}
return toSubagent(row)
}
func (s *Service) Delete(ctx context.Context, id string) error {
pgID, err := parseUUID(id)
if err != nil {
return err
}
return s.queries.SoftDeleteSubagent(ctx, pgID)
}
func toSubagent(row sqlc.Subagent) (Subagent, error) {
messages, err := unmarshalMessages(row.Messages)
if err != nil {
return Subagent{}, err
}
metadata, err := unmarshalMetadata(row.Metadata)
if err != nil {
return Subagent{}, err
}
skills, err := unmarshalSkills(row.Skills)
if err != nil {
return Subagent{}, err
}
item := Subagent{
ID: toUUIDString(row.ID),
Name: row.Name,
Description: row.Description,
UserID: toUUIDString(row.UserID),
Messages: messages,
Metadata: metadata,
Skills: skills,
Deleted: row.Deleted,
}
if row.CreatedAt.Valid {
item.CreatedAt = row.CreatedAt.Time
}
if row.UpdatedAt.Valid {
item.UpdatedAt = row.UpdatedAt.Time
}
if row.DeletedAt.Valid {
deletedAt := row.DeletedAt.Time
item.DeletedAt = &deletedAt
}
return item, nil
}
func marshalMessages(messages []map[string]interface{}) ([]byte, error) {
if messages == nil {
messages = []map[string]interface{}{}
}
return json.Marshal(messages)
}
func unmarshalMessages(payload []byte) ([]map[string]interface{}, error) {
if len(payload) == 0 {
return []map[string]interface{}{}, nil
}
var messages []map[string]interface{}
if err := json.Unmarshal(payload, &messages); err != nil {
return nil, err
}
if messages == nil {
messages = []map[string]interface{}{}
}
return messages, nil
}
func marshalMetadata(metadata map[string]interface{}) ([]byte, error) {
if metadata == nil {
metadata = map[string]interface{}{}
}
return json.Marshal(metadata)
}
func unmarshalMetadata(payload []byte) (map[string]interface{}, error) {
if len(payload) == 0 {
return map[string]interface{}{}, nil
}
var metadata map[string]interface{}
if err := json.Unmarshal(payload, &metadata); err != nil {
return nil, err
}
if metadata == nil {
metadata = map[string]interface{}{}
}
return metadata, nil
}
func marshalSkills(skills []string) ([]byte, error) {
return json.Marshal(normalizeSkills(skills))
}
func unmarshalSkills(payload []byte) ([]string, error) {
if len(payload) == 0 {
return []string{}, nil
}
var skills []string
if err := json.Unmarshal(payload, &skills); err != nil {
return nil, err
}
if skills == nil {
skills = []string{}
}
return skills, nil
}
func normalizeSkills(skills []string) []string {
seen := map[string]struct{}{}
normalized := make([]string, 0, len(skills))
for _, skill := range skills {
trimmed := strings.TrimSpace(skill)
if trimmed == "" {
continue
}
if _, ok := seen[trimmed]; ok {
continue
}
seen[trimmed] = struct{}{}
normalized = append(normalized, trimmed)
}
return normalized
}
func mergeSkills(existing []string, incoming []string) []string {
merged := append([]string{}, existing...)
merged = append(merged, incoming...)
return normalizeSkills(merged)
}
func parseUUID(id string) (pgtype.UUID, error) {
parsed, err := uuid.Parse(strings.TrimSpace(id))
if err != nil {
return pgtype.UUID{}, fmt.Errorf("invalid UUID: %w", err)
}
var pgID pgtype.UUID
pgID.Valid = true
copy(pgID.Bytes[:], parsed[:])
return pgID, nil
}
func toUUIDString(value pgtype.UUID) string {
if !value.Valid {
return ""
}
id, err := uuid.FromBytes(value.Bytes[:])
if err != nil {
return ""
}
return id.String()
}
+56
View File
@@ -0,0 +1,56 @@
package subagent
import "time"
type Subagent struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
UserID string `json:"user_id"`
Messages []map[string]interface{} `json:"messages"`
Metadata map[string]interface{} `json:"metadata"`
Skills []string `json:"skills"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Deleted bool `json:"deleted"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}
type CreateRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Messages []map[string]interface{} `json:"messages,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Skills []string `json:"skills,omitempty"`
}
type UpdateRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type UpdateContextRequest struct {
Messages []map[string]interface{} `json:"messages"`
}
type UpdateSkillsRequest struct {
Skills []string `json:"skills"`
}
type AddSkillsRequest struct {
Skills []string `json:"skills"`
}
type ListResponse struct {
Items []Subagent `json:"items"`
}
type ContextResponse struct {
Messages []map[string]interface{} `json:"messages"`
}
type SkillsResponse struct {
Skills []string `json:"skills"`
}