Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7c0205c50 | |||
| 196b543bee | |||
| 3d4e6d0939 | |||
| f94fddddb1 | |||
| 631bb64109 | |||
| 4f8f2f4190 | |||
| 1689167a7b | |||
| 5cd81f325f | |||
| cd010fc7a1 | |||
| e3a4a3c5c7 | |||
| 4b3f0b1b55 | |||
| e459775740 | |||
| 5718685c40 | |||
| 5c899d1635 | |||
| 8db2141d45 | |||
| 3198600031 | |||
| 81a3470407 | |||
| cfc78e0493 | |||
| c179e014ad |
@@ -18,6 +18,14 @@ jobs:
|
||||
with:
|
||||
go-version: '1.26'
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Enable pnpm
|
||||
run: corepack enable
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -31,9 +39,37 @@ jobs:
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Build UI
|
||||
run: |
|
||||
cd ui
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
|
||||
- name: Tidy modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Build UI
|
||||
run: |
|
||||
cd ui
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
|
||||
|
||||
@@ -36,3 +36,4 @@ ui/.svelte-kit/
|
||||
internal/app/ui/dist/*
|
||||
!internal/app/ui/dist/placeholder.txt
|
||||
.codex
|
||||
.worktrees/
|
||||
|
||||
@@ -65,10 +65,47 @@ The AMCS directory is used to store configuration and code for the Avalon Memory
|
||||
| `add_project_guardrail` | Link a guardrail to a project; pass `project` if client is stateless |
|
||||
| `remove_project_guardrail` | Unlink a guardrail from a project; pass `project` if client is stateless |
|
||||
| `list_project_guardrails` | Guardrails for a project; pass `project` if client is stateless |
|
||||
| `bootstrap_world_model` | Validate and activate a project, then load effective skills, guardrails, personas, and bounded context |
|
||||
| `add_project_persona` | Link a persona to a project and optionally make it the default |
|
||||
| `remove_project_persona` | Unlink a persona from a project |
|
||||
| `set_default_project_persona` | Select the default persona for a project |
|
||||
| `list_project_personas` | List project personas and identify the default |
|
||||
| `get_version_info` | Build version, commit, and date |
|
||||
| `describe_tools` | List all available MCP tools with names, descriptions, categories, and model-authored usage notes; call this at the start of a session to orient yourself |
|
||||
| `annotate_tool` | Persist your own usage notes for a specific tool; notes are returned by `describe_tools` in future sessions |
|
||||
|
||||
## Webhook ingestion
|
||||
|
||||
External automation can create thoughts without speaking MCP by posting JSON to `POST /webhooks/thoughts`. The endpoint is protected by the same AMCS authentication middleware as MCP and file uploads, so pass one configured API key via `x-brain-key`, an authorization bearer token header, or another enabled auth method.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/webhooks/thoughts \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-brain-key: <api-key>' \
|
||||
-H 'Idempotency-Key: n8n-run-123' \
|
||||
-d '{
|
||||
"content": "External system observed build failure on main",
|
||||
"project": "amcs",
|
||||
"source": "n8n",
|
||||
"type": "task",
|
||||
"topics": ["ci", "webhook"],
|
||||
"metadata": {"workflow": "ci-monitor", "run_id": "123"}
|
||||
}'
|
||||
```
|
||||
|
||||
Payload fields:
|
||||
|
||||
- `content` is required and becomes the thought content.
|
||||
- `project` is optional; when present it must match an existing AMCS project.
|
||||
- `source`, `type`, `topics`, `people`, `action_items`, and `dates_mentioned` are normalized into the standard thought metadata schema. Unknown `type` values fall back to `observation`.
|
||||
- `metadata` or `source_metadata` may contain safe source-specific JSON values; unsupported values and overly deep objects are dropped rather than persisted.
|
||||
- `idempotency_key` or the `Idempotency-Key` header can be supplied to make repeated webhook deliveries return the existing thought with `duplicate: true`.
|
||||
- `external_id` is stored under `metadata.webhook.external_id` for source-side traceability.
|
||||
|
||||
Successful new ingestion returns `201` with the created thought. Duplicate idempotency-key delivery returns `200` and the previously created thought. Invalid JSON, missing content, missing/unknown projects, or unauthenticated requests are rejected before persistence. Metadata and embedding enrichment are queued after the thought is stored.
|
||||
|
||||
## Learnings
|
||||
|
||||
Learnings are curated, structured memory records for durable insights you want to keep distinct from raw thoughts. Use them for normalized lessons, decisions, and evidence-backed findings that should be easy to retrieve and review over time.
|
||||
@@ -216,9 +253,9 @@ Use `get_version_info` to retrieve the runtime build metadata:
|
||||
|
||||
## Agent Skills and Guardrails
|
||||
|
||||
Skills and guardrails are reusable agent behaviour instructions and constraints that can be attached to projects.
|
||||
Skills and guardrails are reusable agent behaviour instructions and constraints that can be attached to projects. Projects can also expose personas and a default persona through the world model.
|
||||
|
||||
**At the start of every project session, always call `list_project_skills` and `list_project_guardrails` first.** Use the returned skills and guardrails to guide agent behaviour for that project. Only generate or create new skills/guardrails if none are returned. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`.
|
||||
**At the start of every project session, read `amcs://world-model/intro` and call `bootstrap_world_model` with an explicit project.** Apply its skills, guardrails, and default persona before continuing. If your MCP client does not preserve sessions across calls, keep passing `project` explicitly.
|
||||
|
||||
### Skills
|
||||
|
||||
@@ -243,10 +280,12 @@ Severity levels: `low`, `medium`, `high`, `critical`.
|
||||
Link existing skills and guardrails to a project so they are automatically available when that project is active:
|
||||
|
||||
```json
|
||||
{ "project": "my-project", "skill_id": "<uuid>" }
|
||||
{ "project": "my-project", "guardrail_id": "<uuid>" }
|
||||
{ "project": "my-project", "skill_id": 42, "override": false }
|
||||
{ "project": "my-project", "guardrail_id": 17 }
|
||||
```
|
||||
|
||||
Skills are additive by default. Set `override: true` on a project-skill or persona-skill link only when that skill must replace a lower-precedence skill with the same name. World-model precedence is project, selected persona, then runtime. Guardrails are additive and later layers cannot weaken them.
|
||||
|
||||
## Configuration
|
||||
|
||||
Config is YAML-driven. Copy `configs/config.example.yaml` and set:
|
||||
@@ -417,7 +456,7 @@ Returns `{"file": {...}, "uri": "amcs://files/<id>"}`. Pass `thought_id`/`projec
|
||||
|
||||
### MCP resources
|
||||
|
||||
Stored files are also exposed as MCP resources at `amcs://files/{id}`. MCP clients can read raw binary content directly via `resources/read` without going through `load_file`.
|
||||
Stored files are also exposed as MCP resources at `amcs://files/{id}`. MCP clients can read raw binary content directly via `resources/read` without going through `load_file`. The startup guide is available at `amcs://world-model/intro`.
|
||||
|
||||
### HTTP upload and download
|
||||
|
||||
@@ -471,7 +510,7 @@ metadata_retry:
|
||||
include_archived: false
|
||||
```
|
||||
|
||||
**Search fallback**: when no embeddings exist for the active model in scope, `search_thoughts`, `recall_context`, `get_project_context`, `summarize_thoughts`, and `related_thoughts` automatically fall back to Postgres full-text search so results are never silently empty.
|
||||
**Search fallback**: when no embeddings exist for the active model in scope, `search_thoughts`, `recall_context`, `get_project_context`, `summarize_thoughts`, and `related_thoughts` automatically fall back to Postgres full-text search so results are never silently empty. All five tools include a `retrieval_mode` field in their response (`"semantic"` or `"text"`) so callers can see which path was taken.
|
||||
|
||||
## Client Setup
|
||||
|
||||
|
||||
@@ -23,12 +23,15 @@ auth:
|
||||
keys:
|
||||
- id: "local-client"
|
||||
value: "replace-me"
|
||||
tenant_id: "local"
|
||||
superadmin: true
|
||||
description: "main local client key"
|
||||
oauth:
|
||||
clients:
|
||||
- id: "oauth-client"
|
||||
client_id: ""
|
||||
client_secret: ""
|
||||
superadmin: false
|
||||
description: "optional OAuth client credentials"
|
||||
|
||||
database:
|
||||
|
||||
@@ -21,6 +21,8 @@ auth:
|
||||
keys:
|
||||
- id: "local-client"
|
||||
value: "replace-me"
|
||||
tenant_id: "local"
|
||||
superadmin: true
|
||||
description: "main local client key"
|
||||
oauth:
|
||||
clients:
|
||||
|
||||
@@ -21,6 +21,8 @@ auth:
|
||||
keys:
|
||||
- id: "local-client"
|
||||
value: "replace-me"
|
||||
tenant_id: "local"
|
||||
superadmin: true
|
||||
description: "main local client key"
|
||||
oauth:
|
||||
clients:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# World Model Planning
|
||||
|
||||
Project: AMCS
|
||||
|
||||
AMCS already exposes MCP initialization instructions from `llm/memory.md`, active-project tools, project-linked skills and guardrails, persona manifests, project context, and MCP file resources.
|
||||
|
||||
Recommended design:
|
||||
|
||||
- Keep a concise mandatory world-model bootstrap protocol in MCP initialization instructions.
|
||||
- Add a readable static resource at `amcs://world-model/intro` containing the fuller introduction and project-selection protocol.
|
||||
- Add one `bootstrap_world_model` tool. It accepts an explicit project name or ID, validates it, sets it active when the transport is stateful, and returns one bounded snapshot containing project identity, linked skills, linked guardrails, linked persona manifests/default persona, and recent project context.
|
||||
- Never silently select or create a project. Return structured `project_not_found` or `project_ambiguous` errors with candidate projects; stateless clients must keep passing the project explicitly.
|
||||
- Add project-to-persona associations because the current schema only associates skills and guardrails with projects. Support one optional default persona plus additional available personas. Return manifests at bootstrap and load full persona content on demand.
|
||||
- Merge skills additively by default. Store an explicit `override` flag on project-skill and persona-skill association rows, not on the reusable skill itself. An overriding skill replaces lower-precedence skills with the same stable skill key; unrelated skills remain additive.
|
||||
- Resolve precedence deterministically: project skills form the base, the selected persona is applied next, and explicit runtime/session skills are applied last. At each layer, `override: true` replaces the accumulated entry with the same key while `override: false` adds or de-duplicates it.
|
||||
- Include a snapshot version and generation timestamp so clients can cache and refresh the world model safely.
|
||||
|
||||
Delivery phases:
|
||||
|
||||
1. Fix the existing `list_project_skills` SQL failure: `skillSelectCols` leaves `created_at` and other columns unqualified in a join.
|
||||
2. Define the bootstrap response contract, size limits, precedence rules, and structured errors.
|
||||
3. Add project-persona schema/store/tool support through DBML generation.
|
||||
4. Add the intro resource and initialization instructions.
|
||||
5. Implement and register `bootstrap_world_model` using existing project, skill, guardrail, persona, and context stores.
|
||||
6. Add MCP lifecycle, stateful/stateless, missing/ambiguous project, payload-bound, and resource tests.
|
||||
7. Add admin UI management for project personas/default persona and update client documentation.
|
||||
|
||||
Confirmed decision: skills are additive unless their association has `override: true`. Guardrails remain additive, use the stricter severity when duplicates occur, and cannot be weakened by persona or runtime context.
|
||||
|
||||
AMCS MCP writes and subsequent verification hung during this planning session, so this local log is the required fallback rather than a confirmed remote thought/plan.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Tenant identity management
|
||||
|
||||
The previous tenancy implementation used every API-key ID as its tenant key. This meant that two keys could not share data and old unscoped rows could not be reached by any key after tenancy was enabled.
|
||||
|
||||
Implemented a separate tenant mapping: configured and managed API keys now resolve to an assigned tenant ID, while unassigned configured keys retain their historical key-ID tenant boundary for compatibility. Added tenant, tenant-user, key-assignment, and managed-secret records, with a one-time, explicit legacy adoption endpoint that assigns only `NULL` tenant-key rows to a selected tenant.
|
||||
|
||||
The admin UI now has an Identity page for tenants, users, configured-key assignment, managed-key creation (secret shown once), disabling keys, and legacy adoption. Managed secrets are stored only as SHA-256 hashes. The AMCS MCP capture-thought tool was unavailable in this session, so this local log is the required fallback summary.
|
||||
|
||||
Configured keys also accept an optional `auth.keys[].tenant_id`. This supplies the initial tenant boundary at startup; an explicit assignment saved through the Identity UI overrides it.
|
||||
|
||||
Tenant-owned root records now reference `tenants(id)` through `tenant_id`; tenant scoped resources include skills, guardrails, personas, parts, traits, and character arcs. Tenant-selection in the UI is sent as `X-AMCS-Tenant-ID` for admin/ResolveSpec requests.
|
||||
|
||||
With explicit approval that tenancy data is disposable, the compatibility migration now renames the ownership column from `tenant_key` to `tenant_id` across the historical migration chain.
|
||||
|
||||
Tenant selection now centrally filters every tenant-owned ResolveSpec read, update, and delete in the admin UI, and injects the selected `tenant_id` on create requests.
|
||||
|
||||
Identity mutations now require a configured API key with `superadmin: true`; tenants themselves remain database-managed.
|
||||
|
||||
Fixed the admin sidebar tenant-scope selector flicker. Its focus handler reloaded the tenant list and disabled the native select while its picker was opening; tenants now load on sidebar mount only, so a selection remains open and usable. Validated with `pnpm check` (0 errors, 0 warnings).
|
||||
@@ -0,0 +1,3 @@
|
||||
# ResolveSpec array scanning
|
||||
|
||||
Diagnosed `/api/rs/public/agent_skills` failing to scan `tags text[]`. The generated Bun model correctly uses `sqltypes.SqlStringArray`, which implements `sql.Scanner`, but ResolveSpec's read path supplied a separate scan destination instead of scanning the query's registered Bun model. Updated the vendored ResolveSpec handler to call `ScanModel`, and for single-row reads to set the single record as the query model first. This preserves Bun's model field scanner for PostgreSQL arrays.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Per-user tenancy progress review
|
||||
|
||||
Reviewed `docs/per-user-tenancy-plan.md` against the staged worktree. Identity, tenant mappings, schema/model migrations, and primary project/thought/file/catalog scoping are present. The plan's named hardening targets—learnings, plans, chat histories, thought-learning links, and project persona joins—still have no tenant helper predicates. Only auth/keyring tests are staged; the plan's cross-tenant store/tool and migration test matrix has not yet been added or run.
|
||||
@@ -0,0 +1,161 @@
|
||||
# Per-user tenancy implementation plan
|
||||
|
||||
## Scope and current state
|
||||
|
||||
Gitea issue #10 asks AMCS to isolate memories by user, tenant, or workspace. The current branch already carries the first implementation pass, so this plan records the intended model and the remaining hardening work another worker should verify or finish.
|
||||
|
||||
Observed code paths:
|
||||
|
||||
- Authentication enters through `internal/auth/middleware.go`. API-key auth uses the configured header, defaulting to `x-brain-key`; bearer tokens can resolve through OAuth `TokenStore` or API keyring; HTTP Basic resolves OAuth client credentials.
|
||||
- `internal/auth/middleware.go` writes both `auth.key_id` and `tenancy.tenant_id` into the request context. The tenant key is intentionally opaque and currently equals the authenticated key id or OAuth client id.
|
||||
- `internal/tenancy/tenancy.go` exposes `WithTenantKey` and `KeyFromContext` only; callers should not infer user semantics from the tenant string.
|
||||
- Schema already has `tenant_id` on `projects`, `thoughts`, `stored_files`, `learnings`, `plans`, and `chat_histories` in `schema/*.dbml`.
|
||||
- `internal/store/tenancy.go` provides helper functions for appending tenant predicates to SQL.
|
||||
- Tenant scoping is already present in project, thought, and stored-file store paths. Some other project-owned domains still need explicit tenant enforcement.
|
||||
|
||||
## Tenant/user identity source
|
||||
|
||||
Use the authenticated principal id as the tenant boundary:
|
||||
|
||||
1. API key: resolve token through `auth.Keyring.Lookup`; use returned key id as `tenant_id`.
|
||||
2. OAuth bearer token: resolve token through `TokenStore.Lookup`; use returned client id/key id as `tenant_id`.
|
||||
3. OAuth Basic client credentials: resolve through `OAuthRegistry.Lookup`; use returned client id as `tenant_id`.
|
||||
4. Unauthenticated requests must not get tenant context and must not reach protected MCP/API handlers.
|
||||
|
||||
Do not store raw API keys or bearer tokens in tenant columns. Store only stable configured key ids/client ids. Yes, it is less flashy than inventing an account service before breakfast, but it keeps the trust boundary small and auditable.
|
||||
|
||||
## Required schema/model changes
|
||||
|
||||
Source-of-truth DBML changes:
|
||||
|
||||
- Add nullable `tenant_id text` to all user-owned tables:
|
||||
- `projects`
|
||||
- `thoughts`
|
||||
- `stored_files`
|
||||
- `learnings`
|
||||
- `plans`
|
||||
- `chat_histories`
|
||||
- Add tenant indexes:
|
||||
- single-column `tenant_id` for direct filtering
|
||||
- `(tenant_id, name)` unique on `projects`, replacing global `projects.name` uniqueness
|
||||
- `(tenant_id, project_id)` on `thoughts` for common project-scoped memory lookups
|
||||
- Regenerate SQL migrations and generated models from DBML; do not hand-edit generated Go models except as a temporary debugging step.
|
||||
|
||||
Important follow-up: `agent_skills`, `agent_guardrails`, `agent_personas`, `agent_parts`, traits, and arcs are currently global catalogs. Keep them global unless product requirements say skills/personas are private per tenant. Project join tables inherit protection through tenant-scoped project ids, but direct join queries must verify the project belongs to the tenant.
|
||||
|
||||
## Query scoping strategy
|
||||
|
||||
All request-facing store methods that touch tenant-owned rows must include tenant predicates whenever `tenancy.KeyFromContext(ctx)` returns a key.
|
||||
|
||||
Rules:
|
||||
|
||||
- Inserts must populate `tenant_id` from context.
|
||||
- Gets/updates/deletes by id or guid must add `and tenant_id = $n`.
|
||||
- Lists/searches must add `tenant_id = $n` before other filters.
|
||||
- Joins must scope the tenant-owned root table. Example: project summaries should count only thoughts that belong to the same tenant as the project, not merely thoughts with the same `project_id`.
|
||||
- Background maintenance jobs must either run per tenant, carry tenant context explicitly, or intentionally operate cross-tenant with an internal-only code path documented in the job.
|
||||
|
||||
Already-scoped paths to keep:
|
||||
|
||||
- `internal/store/projects.go`: create/get/list/touch projects.
|
||||
- `internal/store/thoughts.go`: create/list/get/update/delete/archive/search/stats/embedding repair paths.
|
||||
- `internal/store/files.go`: insert/get/list stored files.
|
||||
|
||||
Paths needing audit/hardening:
|
||||
|
||||
- `internal/store/learnings.go`: `CreateLearning`, `GetLearning`, and `ListLearnings` should populate/filter by tenant.
|
||||
- `internal/store/plans.go`: `CreatePlan`, `GetPlan`, `GetPlanDetail`, `UpdatePlan`, `DeletePlan`, `ListPlans`, dependency/related-plan operations, and plan skill/guardrail joins should scope to tenant-owned plans.
|
||||
- `internal/store/chat_histories.go`: chat history create/get/list/update paths should populate/filter by tenant.
|
||||
- `internal/store/thought_learning_links.go`: links traverse tenant-owned thoughts/learnings; queries should join and scope both sides or at least the tenant-owned root.
|
||||
- `internal/store/skills.go` and `internal/store/project_personas.go`: project-scoped joins should verify the project is visible to the current tenant before returning linked global catalog records.
|
||||
- ResolveSpec/admin CRUD endpoints exposing generated models must not bypass tenant-aware store methods. If they use direct generic model access, add middleware-level filter injection or disable tenant-owned tables from generic admin writes.
|
||||
|
||||
## Migration and backfill
|
||||
|
||||
Migration approach for existing single-tenant installs:
|
||||
|
||||
1. Add nullable `tenant_id` columns first; do not make them `not null` initially because existing deployments have historical rows without a principal.
|
||||
2. Backfill existing rows to a configured default tenant only if the instance enables multi-tenant mode or defines `auth.default_tenant_id`. Otherwise leave `NULL` rows visible only to unauthenticated/internal single-tenant contexts.
|
||||
3. Replace global project-name uniqueness with `(tenant_id, name)` uniqueness. For PostgreSQL, preserve legacy `NULL` semantics carefully: multiple null-tenant projects with the same name may be possible unless a partial unique index is added for null tenant rows.
|
||||
4. Add indexes concurrently where practical for production-sized tables.
|
||||
5. Document that after enabling auth-backed tenancy, legacy null-tenant data is not visible to authenticated tenants unless backfilled.
|
||||
|
||||
Recommended config addition:
|
||||
|
||||
- `auth.default_tenant_id` or `tenancy.default_key` for one-time/self-hosted backfill and development.
|
||||
- Optional `tenancy.mode: single|authenticated` so operators can keep current single-user behavior deliberately instead of discovering isolation by accident. An accident in auth is just a breach with better branding.
|
||||
|
||||
## Authorization checks
|
||||
|
||||
Authorization is row ownership by tenant key:
|
||||
|
||||
- A request may only see or mutate rows whose `tenant_id` equals the authenticated tenant key.
|
||||
- Cross-tenant ids/gids should behave as not found, not forbidden, to avoid existence leaks.
|
||||
- Tenant key is server-derived only. Ignore any client-provided `tenant_id` fields on tool/API inputs.
|
||||
- Project id references in create/update operations must be validated against the same tenant before use. This prevents attaching a new thought/file/plan to another tenant's project id.
|
||||
- Relationship operations must verify both endpoints are in the same tenant before creating links.
|
||||
- Global catalogs can be read across tenants only if intentionally shared; project-specific associations must be tenant-checked through the project.
|
||||
|
||||
## Affected endpoints/services/UI surfaces
|
||||
|
||||
Backend/MCP tools:
|
||||
|
||||
- Project tools: create/get/list/set active project.
|
||||
- Thought tools: capture, retrieve, update, delete/archive, semantic search, text search, stats, metadata and embedding repair queues.
|
||||
- File tools and binary file upload/download APIs.
|
||||
- Learning tools and thought-learning link tools.
|
||||
- Plan/task tools including dependencies, related plans, skills, and guardrails.
|
||||
- Chat history/session persistence tools.
|
||||
- Persona/skill/guardrail project-association tools.
|
||||
|
||||
HTTP/API boundaries:
|
||||
|
||||
- MCP SSE and streamable HTTP handlers must run behind auth middleware when auth is configured.
|
||||
- Any non-MCP REST endpoints under the admin/API server must either use tenant-aware stores or explicitly be internal/admin-only.
|
||||
- ResolveSpec admin CRUD views need tenant filter injection or table-level access restrictions for tenant-owned models.
|
||||
|
||||
UI:
|
||||
|
||||
- Project selector/list should naturally show tenant-scoped projects.
|
||||
- Admin tables for projects/thoughts/files/learnings/plans/chat histories must not show `tenant_id` as an editable field.
|
||||
- If a tenant switcher is ever added, it must map to a server-side authenticated principal or admin impersonation path, not a client-side query parameter. Obviously.
|
||||
|
||||
## Test cases
|
||||
|
||||
Minimum test matrix:
|
||||
|
||||
1. Auth middleware propagates tenant key for API-key header auth.
|
||||
2. Auth middleware propagates tenant key for bearer token via OAuth token store.
|
||||
3. Auth middleware propagates tenant key for HTTP Basic OAuth client credentials.
|
||||
4. Tenant A and tenant B can create projects with the same name; each only lists its own project.
|
||||
5. Tenant A cannot get/update/delete/archive Tenant B's thought by guid or numeric id.
|
||||
6. Semantic and text search return only same-tenant thoughts, including project-filtered searches.
|
||||
7. Stored file get/list/download is scoped; a file id from another tenant returns not found.
|
||||
8. Learning create/list/get is scoped, including links to thoughts.
|
||||
9. Plan create/list/get/update/delete and dependency/related-plan operations are scoped.
|
||||
10. Project skill/persona/guardrail association reads verify the project belongs to the tenant.
|
||||
11. Background metadata/embedding retry queues do not cross tenants unless intentionally internal.
|
||||
12. Migration test covers legacy null-tenant rows and backfill to default tenant.
|
||||
13. ResolveSpec/admin API tests prove tenant-owned resources are filtered or unavailable without admin override.
|
||||
|
||||
## Assumptions and blockers
|
||||
|
||||
Assumptions:
|
||||
|
||||
- The first tenancy boundary is authenticated principal id, not human account, org, or workspace. This is consistent with the current auth system and avoids adding an account model prematurely.
|
||||
- Null `tenant_id` remains the compatibility path for existing single-tenant/internal flows.
|
||||
- Global skills/personas/guardrails remain shared catalogs until a separate product decision makes them tenant-private.
|
||||
|
||||
Blockers/decisions needed:
|
||||
|
||||
- Decide whether legacy rows should be backfilled automatically to a configured default tenant during migration or left null until an operator runs an explicit backfill.
|
||||
- Decide whether ResolveSpec admin endpoints are trusted admin-only or must enforce tenant predicates like MCP tools.
|
||||
- Decide whether future OAuth subjects should distinguish user id from client id; the current implementation only has client/key id available.
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. Finish tenant scoping audits for learnings, plans, chat histories, thought-learning links, and project catalog joins.
|
||||
2. Add tests proving cross-tenant invisibility for each store/tool domain before widening coverage. Yes, tests first; future us has enough enemies.
|
||||
3. Add config/backfill migration behavior and document operator steps.
|
||||
4. Lock down or filter ResolveSpec/admin tenant-owned model access.
|
||||
5. Run `make test` and `make build`; include exact results in the issue/PR comment.
|
||||
@@ -3,7 +3,8 @@ module git.warky.dev/wdevs/amcs
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/bitechdev/ResolveSpec v1.1.15
|
||||
git.warky.dev/wdevs/relspecgo v1.0.62
|
||||
github.com/bitechdev/ResolveSpec v1.1.27
|
||||
github.com/google/jsonschema-go v0.4.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
@@ -11,7 +12,7 @@ require (
|
||||
github.com/pgvector/pgvector-go v0.3.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/uptrace/bun v1.2.18
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.18
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.12
|
||||
github.com/uptrace/bunrouter v1.0.23
|
||||
golang.org/x/sync v0.20.0
|
||||
@@ -46,7 +47,6 @@ require (
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.19.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
@@ -62,8 +62,8 @@ require (
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.16 // indirect
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 // indirect
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.18 // indirect
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
|
||||
@@ -2,6 +2,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
|
||||
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
|
||||
git.warky.dev/wdevs/relspecgo v1.0.62 h1:byBe2IlcwQRKsV5qXHfGITtfvGZewHlNstP+O8BGnns=
|
||||
git.warky.dev/wdevs/relspecgo v1.0.62/go.mod h1:JpZBvui9dYc/QcD5TZ1wKab+4bUvPw5/rOLOA/h2F6A=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||
@@ -34,8 +36,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bitechdev/ResolveSpec v1.1.15 h1:Bhy1ZWGUg9AJOhOLk5Di9ilOVKmZ40SkGIGDraAlti8=
|
||||
github.com/bitechdev/ResolveSpec v1.1.15/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs=
|
||||
github.com/bitechdev/ResolveSpec v1.1.27 h1:qugBwR3Qoy4tNKhAyJsFszZE+kRR0gzl5w42XE3/scY=
|
||||
github.com/bitechdev/ResolveSpec v1.1.27/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
@@ -292,12 +294,12 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYm
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/uptrace/bun v1.2.18 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU=
|
||||
github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y=
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.16 h1:rKv0cKPNBviXadB/+2Y/UedA/c1JnwGzUWZkdN5FdSQ=
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.16/go.mod h1:J5U7tGKWDsx2Q7MwDZF2417jCdpD6yD/ZMFJcCR80bk=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E=
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.18 h1:nYzHoyJKJlIyl5i95Exi8ZTK8ooKWG+o3z3f404d/yQ=
|
||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.18/go.mod h1:Su45Je7z66sfeZ3d1ZsnOQEK8xfzGgaMzBvtoE8yFhk=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.18 h1:IZ6nM2+OYrL8lkEAy7UkSEZvoa3vluTAUlZfPtlRB2k=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.18/go.mod h1:Tqdf4QP1okrGYpXfodXvCOK6Ob1OOTwSaoAzCgBB3IU=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 h1:Z33SY/U++XK9uGWqS4h8OZVxfCXguIG+sU9cYq2PGFQ=
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18/go.mod h1:1MVOS/Ncy4FZbkJcgUFH6OqYoQinYNjkEwsmNQEXz2A=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
|
||||
github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI=
|
||||
@@ -452,8 +454,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/auth"
|
||||
)
|
||||
|
||||
type identityAdmin struct {
|
||||
pool *pgxpool.Pool
|
||||
keyring *auth.Keyring
|
||||
oauthRegistry *auth.OAuthRegistry
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func newIdentityAdmin(pool *pgxpool.Pool, keyring *auth.Keyring, oauthRegistry *auth.OAuthRegistry, logger *slog.Logger) *identityAdmin {
|
||||
return &identityAdmin{pool: pool, keyring: keyring, oauthRegistry: oauthRegistry, logger: logger}
|
||||
}
|
||||
|
||||
func (a *identityAdmin) isSuperadmin(keyID string) bool {
|
||||
if a.keyring != nil && a.keyring.IsSuperadmin(keyID) {
|
||||
return true
|
||||
}
|
||||
return a.oauthRegistry != nil && a.oauthRegistry.IsSuperadmin(keyID)
|
||||
}
|
||||
|
||||
func loadIdentityKeyring(ctx context.Context, pool *pgxpool.Pool, keyring *auth.Keyring) error {
|
||||
if keyring == nil {
|
||||
return nil
|
||||
}
|
||||
rows, err := pool.Query(ctx, `select a.key_id, a.tenant_id, a.enabled, coalesce(m.secret_hash, '') from api_key_assignments a left join managed_api_keys m on m.key_id = a.key_id`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var keyID, tenantID, hash string
|
||||
var enabled bool
|
||||
if err := rows.Scan(&keyID, &tenantID, &enabled, &hash); err != nil {
|
||||
return err
|
||||
}
|
||||
keyring.AssignTenant(keyID, tenantID)
|
||||
if hash != "" {
|
||||
keyring.AddManaged(keyID, hash, enabled)
|
||||
}
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// ensureConfiguredTenants makes a tenant referenced in static YAML visible to
|
||||
// the admin UI as well as to the authentication middleware.
|
||||
func ensureConfiguredTenants(ctx context.Context, pool *pgxpool.Pool, keyring *auth.Keyring) error {
|
||||
if keyring == nil {
|
||||
return nil
|
||||
}
|
||||
for _, key := range keyring.ConfiguredKeys() {
|
||||
tenantID := strings.TrimSpace(key.TenantID)
|
||||
if tenantID == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `insert into tenants (id, name) values ($1, $1) on conflict (id) do nothing`, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *identityAdmin) handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
keyID, ok := auth.KeyIDFromContext(r.Context())
|
||||
if !ok || !a.isSuperadmin(keyID) {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "superadmin API key required"})
|
||||
return
|
||||
}
|
||||
}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/admin/identity")
|
||||
switch {
|
||||
case r.Method == http.MethodGet && path == "":
|
||||
a.list(w, r)
|
||||
case r.Method == http.MethodPost && path == "/tenants":
|
||||
a.createTenant(w, r)
|
||||
case r.Method == http.MethodPost && path == "/users":
|
||||
a.createUser(w, r)
|
||||
case r.Method == http.MethodPost && path == "/keys":
|
||||
a.createKey(w, r)
|
||||
case r.Method == http.MethodPatch && strings.HasPrefix(path, "/keys/"):
|
||||
a.updateKey(w, r, strings.TrimPrefix(path, "/keys/"))
|
||||
case r.Method == http.MethodPost && strings.HasPrefix(path, "/tenants/") && strings.HasSuffix(path, "/adopt-legacy"):
|
||||
a.adoptLegacy(w, r, strings.TrimSuffix(strings.TrimPrefix(path, "/tenants/"), "/adopt-legacy"))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type tenantDTO struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
type userDTO struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
type keyDTO struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID *string `json:"user_id,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (a *identityAdmin) list(w http.ResponseWriter, r *http.Request) {
|
||||
result := struct {
|
||||
Tenants []tenantDTO `json:"tenants"`
|
||||
Users []userDTO `json:"users"`
|
||||
Keys []keyDTO `json:"keys"`
|
||||
}{Tenants: []tenantDTO{}, Users: []userDTO{}, Keys: []keyDTO{}}
|
||||
rows, err := a.pool.Query(r.Context(), `select id, name, created_at from tenants order by name`)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var x tenantDTO
|
||||
if err := rows.Scan(&x.ID, &x.Name, &x.CreatedAt); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
result.Tenants = append(result.Tenants, x)
|
||||
}
|
||||
rows, err = a.pool.Query(r.Context(), `select id, tenant_id, name, email, created_at from tenant_users order by name`)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var x userDTO
|
||||
if err := rows.Scan(&x.ID, &x.TenantID, &x.Name, &x.Email, &x.CreatedAt); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
result.Users = append(result.Users, x)
|
||||
}
|
||||
configured := make(map[string]authKey)
|
||||
if a.keyring != nil {
|
||||
for _, key := range a.keyring.ConfiguredKeys() {
|
||||
configured[key.ID] = authKey{description: key.Description}
|
||||
}
|
||||
}
|
||||
rows, err = a.pool.Query(r.Context(), `select key_id, tenant_id, user_id, description, source, enabled, created_at from api_key_assignments order by key_id`)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var x keyDTO
|
||||
if err := rows.Scan(&x.ID, &x.TenantID, &x.UserID, &x.Description, &x.Source, &x.Enabled, &x.CreatedAt); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
result.Keys = append(result.Keys, x)
|
||||
delete(configured, x.ID)
|
||||
}
|
||||
for id, key := range configured {
|
||||
result.Keys = append(result.Keys, keyDTO{ID: id, Description: key.description, Source: "configured", Enabled: true})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
type authKey struct{ description string }
|
||||
|
||||
func (a *identityAdmin) createTenant(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
body.Name = strings.TrimSpace(body.Name)
|
||||
if body.Name == "" {
|
||||
badRequest(w, "name is required")
|
||||
return
|
||||
}
|
||||
x := tenantDTO{ID: newIdentityID(), Name: body.Name}
|
||||
err := a.pool.QueryRow(r.Context(), `insert into tenants (id,name) values ($1,$2) returning created_at`, x.ID, x.Name).Scan(&x.CreatedAt)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, x)
|
||||
}
|
||||
func (a *identityAdmin) createUser(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
TenantID, Name string
|
||||
Email *string `json:"email"`
|
||||
}
|
||||
if !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
body.TenantID = strings.TrimSpace(body.TenantID)
|
||||
body.Name = strings.TrimSpace(body.Name)
|
||||
if body.TenantID == "" || body.Name == "" {
|
||||
badRequest(w, "tenant_id and name are required")
|
||||
return
|
||||
}
|
||||
x := userDTO{ID: newIdentityID(), TenantID: body.TenantID, Name: body.Name, Email: body.Email}
|
||||
err := a.pool.QueryRow(r.Context(), `insert into tenant_users (id,tenant_id,name,email) values ($1,$2,$3,$4) returning created_at`, x.ID, x.TenantID, x.Name, x.Email).Scan(&x.CreatedAt)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, x)
|
||||
}
|
||||
func (a *identityAdmin) createKey(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID *string `json:"user_id"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
body.TenantID = strings.TrimSpace(body.TenantID)
|
||||
if body.TenantID == "" {
|
||||
badRequest(w, "tenant_id is required")
|
||||
return
|
||||
}
|
||||
secret, hash, err := auth.GenerateSecret()
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
x := keyDTO{ID: newIdentityID(), TenantID: body.TenantID, UserID: body.UserID, Description: strings.TrimSpace(body.Description), Source: "managed", Enabled: true}
|
||||
tx, err := a.pool.Begin(r.Context())
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
if err = tx.QueryRow(r.Context(), `insert into api_key_assignments (key_id,tenant_id,user_id,description,source,enabled) values ($1,$2,$3,$4,'managed',true) returning created_at`, x.ID, x.TenantID, x.UserID, x.Description).Scan(&x.CreatedAt); err == nil {
|
||||
_, err = tx.Exec(r.Context(), `insert into managed_api_keys (key_id,secret_hash) values ($1,$2)`, x.ID, hash)
|
||||
}
|
||||
if err == nil {
|
||||
err = tx.Commit(r.Context())
|
||||
}
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
a.keyring.AddManaged(x.ID, hash, true)
|
||||
a.keyring.AssignTenant(x.ID, x.TenantID)
|
||||
writeJSON(w, http.StatusCreated, struct {
|
||||
Key keyDTO `json:"key"`
|
||||
Secret string `json:"secret"`
|
||||
}{x, secret})
|
||||
}
|
||||
func (a *identityAdmin) updateKey(w http.ResponseWriter, r *http.Request, keyID string) {
|
||||
var body struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID *string `json:"user_id"`
|
||||
Description *string `json:"description"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
body.TenantID = strings.TrimSpace(body.TenantID)
|
||||
if body.TenantID == "" {
|
||||
badRequest(w, "tenant_id is required")
|
||||
return
|
||||
}
|
||||
if !a.keyring.IsConfigured(keyID) {
|
||||
var exists bool
|
||||
if err := a.pool.QueryRow(r.Context(), `select exists(select 1 from managed_api_keys where key_id=$1)`, keyID).Scan(&exists); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
badRequest(w, "unknown key id")
|
||||
return
|
||||
}
|
||||
}
|
||||
var x keyDTO
|
||||
err := a.pool.QueryRow(r.Context(), `insert into api_key_assignments (key_id,tenant_id,user_id,description,source,enabled) values ($1,$2,$3,coalesce($4,''),case when $6 then 'configured' else 'managed' end,coalesce($5,true)) on conflict (key_id) do update set tenant_id=excluded.tenant_id,user_id=excluded.user_id,description=coalesce($4,api_key_assignments.description),enabled=coalesce($5,api_key_assignments.enabled) returning key_id,tenant_id,user_id,description,source,enabled,created_at`, keyID, body.TenantID, body.UserID, body.Description, body.Enabled, a.keyring.IsConfigured(keyID)).Scan(&x.ID, &x.TenantID, &x.UserID, &x.Description, &x.Source, &x.Enabled, &x.CreatedAt)
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
a.keyring.AssignTenant(x.ID, x.TenantID)
|
||||
a.keyring.SetManagedEnabled(x.ID, x.Enabled)
|
||||
writeJSON(w, http.StatusOK, x)
|
||||
}
|
||||
func (a *identityAdmin) adoptLegacy(w http.ResponseWriter, r *http.Request, tenantID string) {
|
||||
tenantID = strings.TrimSpace(tenantID)
|
||||
if tenantID == "" {
|
||||
badRequest(w, "tenant id is required")
|
||||
return
|
||||
}
|
||||
tables := []string{"projects", "thoughts", "stored_files", "learnings", "plans", "chat_histories"}
|
||||
tx, err := a.pool.Begin(r.Context())
|
||||
if err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
var exists bool
|
||||
if err = tx.QueryRow(r.Context(), `select exists(select 1 from tenants where id=$1)`, tenantID).Scan(&exists); err == nil && !exists {
|
||||
badRequest(w, "tenant does not exist")
|
||||
return
|
||||
}
|
||||
for _, table := range tables {
|
||||
if _, err = tx.Exec(r.Context(), fmt.Sprintf("update %s set tenant_id=$1 where tenant_id is null", table), tenantID); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = tx.Commit(r.Context()); err != nil {
|
||||
identityError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
func newIdentityID() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, v any) bool {
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||
badRequest(w, "invalid JSON")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
func badRequest(w http.ResponseWriter, message string) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": message})
|
||||
}
|
||||
func identityError(w http.ResponseWriter, err error) {
|
||||
if a, ok := err.(interface{ SQLState() string }); ok && a.SQLState() == "23505" {
|
||||
badRequest(w, "that value already exists")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "identity operation failed"})
|
||||
}
|
||||
+36
-20
@@ -91,6 +91,14 @@ func Run(ctx context.Context, configPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
keyring = auth.NewManagedKeyring()
|
||||
}
|
||||
if err := ensureConfiguredTenants(ctx, db.Pool(), keyring); err != nil {
|
||||
return fmt.Errorf("create configured tenants: %w", err)
|
||||
}
|
||||
if err := loadIdentityKeyring(ctx, db.Pool(), keyring); err != nil {
|
||||
return fmt.Errorf("load identity key assignments: %w", err)
|
||||
}
|
||||
tokenStore = auth.NewTokenStore(0)
|
||||
if len(cfg.Auth.OAuth.Clients) > 0 {
|
||||
@@ -192,28 +200,33 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
||||
enrichmentRetryer := tools.NewEnrichmentRetryer(context.Background(), db, bgMetadata, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger)
|
||||
backfillTool := tools.NewBackfillTool(db, bgEmbeddings, activeProjects, logger)
|
||||
adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger)
|
||||
identityAdmin := newIdentityAdmin(db.Pool(), keyring, oauthRegistry, logger)
|
||||
|
||||
toolSet := mcpserver.ToolSet{
|
||||
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
|
||||
Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
|
||||
List: tools.NewListTool(db, cfg.Search, activeProjects),
|
||||
Stats: tools.NewStatsTool(db),
|
||||
Get: tools.NewGetTool(db),
|
||||
Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
|
||||
Delete: tools.NewDeleteTool(db),
|
||||
Archive: tools.NewArchiveTool(db),
|
||||
Projects: tools.NewProjectsTool(db, activeProjects),
|
||||
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
||||
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
||||
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
|
||||
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
||||
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
||||
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
||||
Links: tools.NewLinksTool(db, embeddings, cfg.Search),
|
||||
Files: filesTool,
|
||||
Backfill: backfillTool,
|
||||
Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger),
|
||||
RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer),
|
||||
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
|
||||
Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
|
||||
List: tools.NewListTool(db, cfg.Search, activeProjects),
|
||||
Stats: tools.NewStatsTool(db),
|
||||
Get: tools.NewGetTool(db),
|
||||
Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
|
||||
Delete: tools.NewDeleteTool(db),
|
||||
Archive: tools.NewArchiveTool(db),
|
||||
DuplicateAudit: tools.NewDuplicateAuditTool(db, cfg.Search, activeProjects),
|
||||
Projects: tools.NewProjectsTool(db, activeProjects),
|
||||
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
||||
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
||||
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
|
||||
ProjectPersonas: tools.NewProjectPersonasTool(db, activeProjects),
|
||||
WorldModel: tools.NewWorldModelTool(db, activeProjects),
|
||||
ThoughtLearningLinks: tools.NewThoughtLearningLinksTool(db),
|
||||
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
||||
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
||||
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
||||
Links: tools.NewLinksTool(db, embeddings, cfg.Search),
|
||||
Files: filesTool,
|
||||
Backfill: backfillTool,
|
||||
Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger),
|
||||
RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer),
|
||||
//Maintenance: tools.NewMaintenanceTool(db),
|
||||
Skills: tools.NewSkillsTool(db, activeProjects),
|
||||
Personas: tools.NewAgentPersonasTool(db),
|
||||
@@ -235,12 +248,15 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
||||
}
|
||||
mux.Handle("/files", authMiddleware(fileHandler(filesTool)))
|
||||
mux.Handle("/files/{id}", authMiddleware(fileHandler(filesTool)))
|
||||
mux.Handle("/webhooks/thoughts", authMiddleware(newWebhookThoughtHandler(db, embeddings, cfg.Capture, enrichmentRetryer, backfillTool)))
|
||||
mux.HandleFunc("/.well-known/oauth-authorization-server", oauthMetadataHandler())
|
||||
mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
|
||||
mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
|
||||
mux.HandleFunc("/api/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger))
|
||||
mux.Handle("/api/admin/actions/backfill", authMiddleware(adminActions.backfillHandler()))
|
||||
mux.Handle("/api/admin/actions/retry-metadata", authMiddleware(adminActions.retryMetadataHandler()))
|
||||
mux.Handle("/api/admin/identity", authMiddleware(identityAdmin.handler()))
|
||||
mux.Handle("/api/admin/identity/", authMiddleware(identityAdmin.handler()))
|
||||
mux.HandleFunc("/favicon.ico", serveFavicon)
|
||||
mux.HandleFunc("/images/project.jpg", serveHomeImage)
|
||||
mux.HandleFunc("/images/icon.png", serveIcon)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/uptrace/bunrouter"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
"git.warky.dev/wdevs/amcs/internal/tenancy"
|
||||
)
|
||||
|
||||
func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware func(http.Handler) http.Handler, logger *slog.Logger) error {
|
||||
@@ -45,7 +46,12 @@ func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware
|
||||
rsMount.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
middleware(rsMount).ServeHTTP(w, r)
|
||||
middleware(http.HandlerFunc(func(w http.ResponseWriter, authenticated *http.Request) {
|
||||
if tenantID := strings.TrimSpace(authenticated.Header.Get("X-AMCS-Tenant-ID")); tenantID != "" {
|
||||
authenticated = authenticated.WithContext(tenancy.WithTenantKey(authenticated.Context(), tenantID))
|
||||
}
|
||||
rsMount.ServeHTTP(w, authenticated)
|
||||
})).ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
mux.Handle("/api/rs/", protectedRSMount)
|
||||
|
||||
@@ -14,12 +14,14 @@ func resolveSpecModels() []resolveSpecModel {
|
||||
{schema: "public", entity: "agent_personas", model: generatedmodels.ModelPublicAgentPersonas{}},
|
||||
{schema: "public", entity: "agent_skills", model: generatedmodels.ModelPublicAgentSkills{}},
|
||||
{schema: "public", entity: "agent_traits", model: generatedmodels.ModelPublicAgentTraits{}},
|
||||
{schema: "public", entity: "api_key_assignments", model: generatedmodels.ModelPublicAPIKeyAssignments{}},
|
||||
{schema: "public", entity: "arc_stage_parts", model: generatedmodels.ModelPublicArcStageParts{}},
|
||||
{schema: "public", entity: "arc_stages", model: generatedmodels.ModelPublicArcStages{}},
|
||||
{schema: "public", entity: "character_arcs", model: generatedmodels.ModelPublicCharacterArcs{}},
|
||||
{schema: "public", entity: "chat_histories", model: generatedmodels.ModelPublicChatHistories{}},
|
||||
{schema: "public", entity: "embeddings", model: generatedmodels.ModelPublicEmbeddings{}},
|
||||
{schema: "public", entity: "learnings", model: generatedmodels.ModelPublicLearnings{}},
|
||||
{schema: "public", entity: "managed_api_keys", model: generatedmodels.ModelPublicManagedAPIKeys{}},
|
||||
{schema: "public", entity: "oauth_clients", model: generatedmodels.ModelPublicOauthClients{}},
|
||||
{schema: "public", entity: "persona_arc", model: generatedmodels.ModelPublicPersonaArc{}},
|
||||
{schema: "public", entity: "plan_dependencies", model: generatedmodels.ModelPublicPlanDependencies{}},
|
||||
@@ -28,9 +30,13 @@ func resolveSpecModels() []resolveSpecModel {
|
||||
{schema: "public", entity: "plan_skills", model: generatedmodels.ModelPublicPlanSkills{}},
|
||||
{schema: "public", entity: "plans", model: generatedmodels.ModelPublicPlans{}},
|
||||
{schema: "public", entity: "project_guardrails", model: generatedmodels.ModelPublicProjectGuardrails{}},
|
||||
{schema: "public", entity: "project_personas", model: generatedmodels.ModelPublicProjectPersonas{}},
|
||||
{schema: "public", entity: "project_skills", model: generatedmodels.ModelPublicProjectSkills{}},
|
||||
{schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}},
|
||||
{schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}},
|
||||
{schema: "public", entity: "tenant_users", model: generatedmodels.ModelPublicTenantUsers{}},
|
||||
{schema: "public", entity: "tenants", model: generatedmodels.ModelPublicTenants{}},
|
||||
{schema: "public", entity: "thought_learning_links", model: generatedmodels.ModelPublicThoughtLearningLinks{}},
|
||||
{schema: "public", entity: "thought_links", model: generatedmodels.ModelPublicThoughtLinks{}},
|
||||
{schema: "public", entity: "thoughts", model: generatedmodels.ModelPublicThoughts{}},
|
||||
{schema: "public", entity: "tool_annotations", model: generatedmodels.ModelPublicToolAnnotations{}},
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
var (
|
||||
//go:embed ui/dist
|
||||
uiFiles embed.FS
|
||||
uiDistFS fs.FS
|
||||
uiFiles embed.FS
|
||||
uiDistFS fs.FS
|
||||
indexHTML []byte
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/ai"
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/metadata"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
"git.warky.dev/wdevs/amcs/internal/tools"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
const maxWebhookBodyBytes = 1 << 20
|
||||
|
||||
type webhookThoughtRequest struct {
|
||||
Content string `json:"content"`
|
||||
Project string `json:"project,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Topics []string `json:"topics,omitempty"`
|
||||
People []string `json:"people,omitempty"`
|
||||
ActionItems []string `json:"action_items,omitempty"`
|
||||
DatesMentioned []string `json:"dates_mentioned,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
SourceMetadata map[string]any `json:"source_metadata,omitempty"`
|
||||
IDempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
ExternalID string `json:"external_id,omitempty"`
|
||||
}
|
||||
|
||||
type webhookThoughtResponse struct {
|
||||
Thought thoughttypes.Thought `json:"thought"`
|
||||
Duplicate bool `json:"duplicate"`
|
||||
WebhookMeta thoughttypes.WebhookMetadata `json:"webhook"`
|
||||
}
|
||||
|
||||
type webhookThoughtHandler struct {
|
||||
store *store.DB
|
||||
embeddings *ai.EmbeddingRunner
|
||||
capture config.CaptureConfig
|
||||
retryer tools.MetadataQueuer
|
||||
embedRetryer tools.EmbeddingQueuer
|
||||
}
|
||||
|
||||
func newWebhookThoughtHandler(db *store.DB, embeddings *ai.EmbeddingRunner, capture config.CaptureConfig, retryer tools.MetadataQueuer, embedRetryer tools.EmbeddingQueuer) http.Handler {
|
||||
return &webhookThoughtHandler{store: db, embeddings: embeddings, capture: capture, retryer: retryer, embedRetryer: embedRetryer}
|
||||
}
|
||||
|
||||
func (h *webhookThoughtHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/webhooks/thoughts" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxWebhookBodyBytes)
|
||||
in, err := parseWebhookThoughtRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
webhookMeta := buildWebhookMetadata(in, r.Header.Get("Idempotency-Key"), time.Now().UTC())
|
||||
if webhookMeta.IDempotencyKey != "" {
|
||||
if existing, err := h.store.GetThoughtByWebhookIDempotencyKey(r.Context(), webhookMeta.IDempotencyKey); err == nil {
|
||||
writeWebhookThoughtResponse(w, http.StatusOK, webhookThoughtResponse{Thought: existing, Duplicate: true, WebhookMeta: webhookMeta})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
projectID, err := h.resolveWebhookProject(r, in.Project)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
thought := thoughttypes.Thought{
|
||||
Content: strings.TrimSpace(in.Content),
|
||||
Metadata: normalizeWebhookThoughtMetadata(in, webhookMeta, h.capture),
|
||||
ProjectID: projectID,
|
||||
}
|
||||
created, err := h.store.InsertThought(r.Context(), thought, h.embeddings.PrimaryModel())
|
||||
if err != nil {
|
||||
http.Error(w, "insert thought: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if projectID != nil {
|
||||
_ = h.store.TouchProject(r.Context(), *projectID)
|
||||
}
|
||||
if h.retryer != nil {
|
||||
h.retryer.QueueThought(created.ID)
|
||||
}
|
||||
if h.embedRetryer != nil {
|
||||
h.embedRetryer.QueueThought(r.Context(), created.ID, created.Content)
|
||||
}
|
||||
|
||||
writeWebhookThoughtResponse(w, http.StatusCreated, webhookThoughtResponse{Thought: created, WebhookMeta: webhookMeta})
|
||||
}
|
||||
|
||||
func parseWebhookThoughtRequest(r *http.Request) (webhookThoughtRequest, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), "application/json") {
|
||||
return webhookThoughtRequest{}, errors.New("webhook requires application/json")
|
||||
}
|
||||
defer r.Body.Close()
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
var in webhookThoughtRequest
|
||||
if err := decoder.Decode(&in); err != nil {
|
||||
return webhookThoughtRequest{}, err
|
||||
}
|
||||
if strings.TrimSpace(in.Content) == "" {
|
||||
return webhookThoughtRequest{}, errors.New("content is required")
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func (h *webhookThoughtHandler) resolveWebhookProject(r *http.Request, projectName string) (*int64, error) {
|
||||
projectName = strings.TrimSpace(projectName)
|
||||
if projectName == "" {
|
||||
return nil, nil
|
||||
}
|
||||
project, err := h.store.GetProject(r.Context(), projectName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &project.NumericID, nil
|
||||
}
|
||||
|
||||
func buildWebhookMetadata(in webhookThoughtRequest, headerKey string, now time.Time) thoughttypes.WebhookMetadata {
|
||||
sourceMetadata := in.SourceMetadata
|
||||
if len(sourceMetadata) == 0 {
|
||||
sourceMetadata = in.Metadata
|
||||
}
|
||||
return thoughttypes.WebhookMetadata{
|
||||
ReceivedAt: now.Format(time.RFC3339),
|
||||
IDempotencyKey: firstNonEmpty(in.IDempotencyKey, headerKey),
|
||||
ExternalID: strings.TrimSpace(in.ExternalID),
|
||||
SourceMetadata: sanitizeWebhookMetadata(sourceMetadata),
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeWebhookThoughtMetadata(in webhookThoughtRequest, webhookMeta thoughttypes.WebhookMetadata, capture config.CaptureConfig) thoughttypes.ThoughtMetadata {
|
||||
return metadata.Normalize(thoughttypes.ThoughtMetadata{
|
||||
People: in.People,
|
||||
ActionItems: in.ActionItems,
|
||||
DatesMentioned: in.DatesMentioned,
|
||||
Topics: in.Topics,
|
||||
Type: in.Type,
|
||||
Source: firstNonEmpty(in.Source, "webhook"),
|
||||
Webhook: &webhookMeta,
|
||||
}, capture)
|
||||
}
|
||||
|
||||
func sanitizeWebhookMetadata(in map[string]any) map[string]any {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(in))
|
||||
for key, value := range in {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if sanitized, ok := sanitizeWebhookMetadataValue(value, 0); ok {
|
||||
out[key] = sanitized
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sanitizeWebhookMetadataValue(value any, depth int) (any, bool) {
|
||||
if depth > 3 {
|
||||
return nil, false
|
||||
}
|
||||
switch v := value.(type) {
|
||||
case nil, bool, float64, string:
|
||||
return v, true
|
||||
case []any:
|
||||
if len(v) > 50 {
|
||||
v = v[:50]
|
||||
}
|
||||
out := make([]any, 0, len(v))
|
||||
for _, item := range v {
|
||||
if sanitized, ok := sanitizeWebhookMetadataValue(item, depth+1); ok {
|
||||
out = append(out, sanitized)
|
||||
}
|
||||
}
|
||||
return out, true
|
||||
case map[string]any:
|
||||
if len(v) > 50 {
|
||||
return nil, false
|
||||
}
|
||||
return sanitizeWebhookMetadata(v), true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func writeWebhookThoughtResponse(w http.ResponseWriter, status int, out webhookThoughtResponse) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
)
|
||||
|
||||
func TestParseWebhookThoughtRequestRequiresJSON(t *testing.T) {
|
||||
req := httptestRequest("text/plain", `{"content":"hello"}`)
|
||||
|
||||
_, err := parseWebhookThoughtRequest(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-json content type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWebhookThoughtRequestRequiresContent(t *testing.T) {
|
||||
req := httptestRequest("application/json", `{"source":"n8n"}`)
|
||||
|
||||
_, err := parseWebhookThoughtRequest(req)
|
||||
if err == nil || !strings.Contains(err.Error(), "content is required") {
|
||||
t.Fatalf("error = %v, want content required", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWebhookMetadataUsesHeaderIdempotencyAndSanitizesMetadata(t *testing.T) {
|
||||
now := time.Date(2026, 7, 15, 4, 0, 0, 0, time.UTC)
|
||||
got := buildWebhookMetadata(webhookThoughtRequest{
|
||||
ExternalID: " ext-1 ",
|
||||
Metadata: map[string]any{
|
||||
"service": "n8n",
|
||||
"unsafe": struct{}{},
|
||||
"nested": map[string]any{"ok": true},
|
||||
},
|
||||
}, " key-1 ", now)
|
||||
|
||||
if got.IDempotencyKey != "key-1" {
|
||||
t.Fatalf("IDempotencyKey = %q, want key-1", got.IDempotencyKey)
|
||||
}
|
||||
if got.ExternalID != "ext-1" {
|
||||
t.Fatalf("ExternalID = %q, want ext-1", got.ExternalID)
|
||||
}
|
||||
if got.ReceivedAt != "2026-07-15T04:00:00Z" {
|
||||
t.Fatalf("ReceivedAt = %q", got.ReceivedAt)
|
||||
}
|
||||
if _, ok := got.SourceMetadata["unsafe"]; ok {
|
||||
t.Fatal("unsafe metadata value was not removed")
|
||||
}
|
||||
if got.SourceMetadata["service"] != "n8n" {
|
||||
t.Fatalf("service metadata = %#v", got.SourceMetadata["service"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWebhookThoughtMetadata(t *testing.T) {
|
||||
webhookMeta := buildWebhookMetadata(webhookThoughtRequest{IDempotencyKey: "abc"}, "", time.Date(2026, 7, 15, 4, 0, 0, 0, time.UTC))
|
||||
got := normalizeWebhookThoughtMetadata(webhookThoughtRequest{
|
||||
Source: "github",
|
||||
Type: "task",
|
||||
Topics: []string{"ci", "ci", ""},
|
||||
People: []string{" Sam "},
|
||||
}, webhookMeta, config.CaptureConfig{})
|
||||
|
||||
if got.Source != "github" {
|
||||
t.Fatalf("Source = %q, want github", got.Source)
|
||||
}
|
||||
if got.Type != "task" {
|
||||
t.Fatalf("Type = %q, want task", got.Type)
|
||||
}
|
||||
if len(got.Topics) != 1 || got.Topics[0] != "ci" {
|
||||
t.Fatalf("Topics = %#v, want [ci]", got.Topics)
|
||||
}
|
||||
if got.Webhook == nil || got.Webhook.IDempotencyKey != "abc" {
|
||||
t.Fatalf("Webhook = %#v, want idempotency key abc", got.Webhook)
|
||||
}
|
||||
}
|
||||
|
||||
func httptestRequest(contentType, body string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/webhooks/thoughts", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
return req
|
||||
}
|
||||
+113
-2
@@ -1,14 +1,27 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
)
|
||||
|
||||
type Keyring struct {
|
||||
keys []config.APIKey
|
||||
mu sync.RWMutex
|
||||
keys []config.APIKey
|
||||
tenantsByKeyID map[string]string
|
||||
managed map[string]managedKey
|
||||
}
|
||||
|
||||
type managedKey struct {
|
||||
secretHash string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func NewKeyring(keys []config.APIKey) (*Keyring, error) {
|
||||
@@ -16,14 +29,112 @@ func NewKeyring(keys []config.APIKey) (*Keyring, error) {
|
||||
return nil, fmt.Errorf("keyring requires at least one key")
|
||||
}
|
||||
|
||||
return &Keyring{keys: append([]config.APIKey(nil), keys...)}, nil
|
||||
tenantsByKeyID := make(map[string]string)
|
||||
for _, key := range keys {
|
||||
if tenantID := strings.TrimSpace(key.TenantID); tenantID != "" {
|
||||
tenantsByKeyID[key.ID] = tenantID
|
||||
}
|
||||
}
|
||||
return &Keyring{keys: append([]config.APIKey(nil), keys...), tenantsByKeyID: tenantsByKeyID, managed: make(map[string]managedKey)}, nil
|
||||
}
|
||||
|
||||
// NewManagedKeyring is used when API credentials are administered in the
|
||||
// database rather than supplied through static configuration.
|
||||
func NewManagedKeyring() *Keyring {
|
||||
return &Keyring{tenantsByKeyID: make(map[string]string), managed: make(map[string]managedKey)}
|
||||
}
|
||||
|
||||
func (k *Keyring) Lookup(value string) (string, bool) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
for _, key := range k.keys {
|
||||
if subtle.ConstantTimeCompare([]byte(key.Value), []byte(value)) == 1 {
|
||||
return key.ID, true
|
||||
}
|
||||
}
|
||||
hash := secretHash(value)
|
||||
for keyID, key := range k.managed {
|
||||
if key.enabled && subtle.ConstantTimeCompare([]byte(key.secretHash), []byte(hash)) == 1 {
|
||||
return keyID, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// TenantForKey returns the tenant boundary assigned to keyID. Unassigned
|
||||
// configured keys retain the historical key-ID boundary for compatibility.
|
||||
func (k *Keyring) TenantForKey(keyID string) string {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
if tenantID := k.tenantsByKeyID[keyID]; tenantID != "" {
|
||||
return tenantID
|
||||
}
|
||||
return keyID
|
||||
}
|
||||
|
||||
func (k *Keyring) AssignTenant(keyID, tenantID string) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
if tenantID == "" {
|
||||
delete(k.tenantsByKeyID, keyID)
|
||||
return
|
||||
}
|
||||
k.tenantsByKeyID[keyID] = tenantID
|
||||
}
|
||||
|
||||
func (k *Keyring) AddManaged(keyID, secretHash string, enabled bool) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.managed[keyID] = managedKey{secretHash: secretHash, enabled: enabled}
|
||||
}
|
||||
|
||||
func (k *Keyring) ConfiguredKeys() []config.APIKey {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return append([]config.APIKey(nil), k.keys...)
|
||||
}
|
||||
|
||||
func (k *Keyring) IsConfigured(keyID string) bool {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
for _, key := range k.keys {
|
||||
if key.ID == keyID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keyring) IsSuperadmin(keyID string) bool {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
for _, key := range k.keys {
|
||||
if key.ID == keyID {
|
||||
return key.Superadmin
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keyring) SetManagedEnabled(keyID string, enabled bool) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
if key, ok := k.managed[keyID]; ok {
|
||||
key.enabled = enabled
|
||||
k.managed[keyID] = key
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateSecret() (string, string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
secret := "amcs_" + hex.EncodeToString(b)
|
||||
return secret, secretHash(secret), nil
|
||||
}
|
||||
|
||||
func secretHash(secret string) string {
|
||||
sum := sha256.Sum256([]byte(secret))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
@@ -36,6 +36,26 @@ func TestNewKeyringAndLookup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredKeyUsesTenantID(t *testing.T) {
|
||||
keyring, err := NewKeyring([]config.APIKey{{ID: "agent-key", Value: "secret", TenantID: "acme"}})
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeyring() error = %v", err)
|
||||
}
|
||||
if got := keyring.TenantForKey("agent-key"); got != "acme" {
|
||||
t.Fatalf("TenantForKey() = %q, want acme", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredKeySuperadmin(t *testing.T) {
|
||||
keyring, err := NewKeyring([]config.APIKey{{ID: "operator", Value: "secret", Superadmin: true}})
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeyring() error = %v", err)
|
||||
}
|
||||
if !keyring.IsSuperadmin("operator") || keyring.IsSuperadmin("missing") {
|
||||
t.Fatal("IsSuperadmin() did not return the configured role")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareAllowsHeaderAuthAndSetsContext(t *testing.T) {
|
||||
keyring, err := NewKeyring([]config.APIKey{{ID: "client-a", Value: "secret"}})
|
||||
if err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/observability"
|
||||
"git.warky.dev/wdevs/amcs/internal/requestip"
|
||||
"git.warky.dev/wdevs/amcs/internal/tenancy"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
@@ -50,6 +51,14 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
||||
)
|
||||
}
|
||||
}
|
||||
withTenant := func(ctx context.Context, keyID string) context.Context {
|
||||
ctx = context.WithValue(ctx, keyIDContextKey, keyID)
|
||||
tenantID := keyID
|
||||
if keyring != nil {
|
||||
tenantID = keyring.TenantForKey(keyID)
|
||||
}
|
||||
return tenancy.WithTenantKey(ctx, tenantID)
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
remoteAddr := requestip.FromRequest(r)
|
||||
@@ -63,7 +72,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
||||
return
|
||||
}
|
||||
recordAccess(r, keyID)
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
||||
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -73,14 +82,14 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
||||
if tokenStore != nil {
|
||||
if keyID, ok := tokenStore.Lookup(bearer); ok {
|
||||
recordAccess(r, keyID)
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
||||
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||
return
|
||||
}
|
||||
}
|
||||
if keyring != nil {
|
||||
if keyID, ok := keyring.Lookup(bearer); ok {
|
||||
recordAccess(r, keyID)
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
||||
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -103,7 +112,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
||||
return
|
||||
}
|
||||
recordAccess(r, keyID)
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
||||
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,7 +126,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
||||
return
|
||||
}
|
||||
recordAccess(r, keyID)
|
||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
||||
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,3 +31,18 @@ func (o *OAuthRegistry) Lookup(clientID string, clientSecret string) (string, bo
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// IsSuperadmin reports whether keyID (as returned by Lookup) belongs to an
|
||||
// OAuth client configured with superadmin: true.
|
||||
func (o *OAuthRegistry) IsSuperadmin(keyID string) bool {
|
||||
for _, client := range o.clients {
|
||||
id := client.ID
|
||||
if id == "" {
|
||||
id = client.ClientID
|
||||
}
|
||||
if id == keyID {
|
||||
return client.Superadmin
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -32,6 +32,26 @@ func TestNewOAuthRegistryAndLookup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthRegistryIsSuperadmin(t *testing.T) {
|
||||
registry, err := NewOAuthRegistry([]config.OAuthClient{
|
||||
{ID: "oauth-admin", ClientID: "admin-id", ClientSecret: "admin-secret", Superadmin: true},
|
||||
{ID: "oauth-client", ClientID: "client-id", ClientSecret: "client-secret"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewOAuthRegistry() error = %v", err)
|
||||
}
|
||||
|
||||
if !registry.IsSuperadmin("oauth-admin") {
|
||||
t.Fatal("IsSuperadmin(oauth-admin) = false, want true")
|
||||
}
|
||||
if registry.IsSuperadmin("oauth-client") {
|
||||
t.Fatal("IsSuperadmin(oauth-client) = true, want false")
|
||||
}
|
||||
if registry.IsSuperadmin("unknown") {
|
||||
t.Fatal("IsSuperadmin(unknown) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareAllowsOAuthBasicAuthAndSetsContext(t *testing.T) {
|
||||
oauthRegistry, err := NewOAuthRegistry([]config.OAuthClient{{
|
||||
ID: "oauth-client",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/tenancy"
|
||||
)
|
||||
|
||||
func TestMiddlewareAddsTenantKeyToContext(t *testing.T) {
|
||||
keyring, err := NewKeyring([]config.APIKey{{ID: "user-a", Value: "secret-a"}})
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeyring error = %v", err)
|
||||
}
|
||||
|
||||
var gotKeyID, gotTenant string
|
||||
handler := Middleware(config.AuthConfig{}, keyring, nil, nil, nil, slog.Default())(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var ok bool
|
||||
gotKeyID, ok = KeyIDFromContext(r.Context())
|
||||
if !ok {
|
||||
t.Fatal("KeyIDFromContext ok = false")
|
||||
}
|
||||
gotTenant, ok = tenancy.KeyFromContext(r.Context())
|
||||
if !ok {
|
||||
t.Fatal("tenancy.KeyFromContext ok = false")
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/mcp", nil)
|
||||
req.Header.Set("x-brain-key", "secret-a")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusNoContent {
|
||||
t.Fatalf("status = %d, want %d", rr.Code, http.StatusNoContent)
|
||||
}
|
||||
if gotKeyID != "user-a" || gotTenant != "user-a" {
|
||||
t.Fatalf("keyID=%q tenant=%q, want user-a/user-a", gotKeyID, gotTenant)
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@ type AuthConfig struct {
|
||||
type APIKey struct {
|
||||
ID string `yaml:"id"`
|
||||
Value string `yaml:"value"`
|
||||
TenantID string `yaml:"tenant_id"`
|
||||
Superadmin bool `yaml:"superadmin"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
@@ -64,6 +66,7 @@ type OAuthClient struct {
|
||||
ID string `yaml:"id"`
|
||||
ClientID string `yaml:"client_id"`
|
||||
ClientSecret string `yaml:"client_secret"`
|
||||
Superadmin bool `yaml:"superadmin"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,23 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentGuardrails struct {
|
||||
bun.BaseModel `bun:"table:public.agent_guardrails,alias:agent_guardrails"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Severity resolvespec_common.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
|
||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Severity sql_types.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelGuardrailIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails
|
||||
RelGuardrailIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
|
||||
RelGuardrailIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
|
||||
@@ -45,7 +47,7 @@ func (m ModelPublicAgentGuardrails) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentGuardrails) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,24 +3,26 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentParts struct {
|
||||
bun.BaseModel `bun:"table:public.agent_parts,alias:agent_parts"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Content resolvespec_common.SqlString `bun:"content,type:text,default:'',notnull," json:"content"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
PartType resolvespec_common.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
|
||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelPartIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
|
||||
RelPartIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Content sql_types.SqlString `bun:"content,type:text,default:'',notnull," json:"content"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
PartType sql_types.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelPartIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
|
||||
RelPartIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAgentParts
|
||||
@@ -45,7 +47,7 @@ func (m ModelPublicAgentParts) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentParts) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,13 +3,13 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentPersonaGuardrails struct {
|
||||
bun.BaseModel `bun:"table:public.agent_persona_guardrails,alias:agent_persona_guardrails"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull," json:"guardrail_id"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
|
||||
@@ -38,7 +38,7 @@ func (m ModelPublicAgentPersonaGuardrails) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentPersonaGuardrails) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,19 +3,19 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentPersonaParts struct {
|
||||
bun.BaseModel `bun:"table:public.agent_persona_parts,alias:agent_persona_parts"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_id"`
|
||||
PartOrder int32 `bun:"part_order,type:int,default:0,notnull," json:"part_order"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
Priority int32 `bun:"priority,type:int,default:0,notnull," json:"priority"`
|
||||
RelPartID *ModelPublicAgentParts `bun:"rel:has-one,join:part_id=id" json:"relpartid,omitempty"` // Has one ModelPublicAgentParts
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_id"`
|
||||
PartOrder int32 `bun:"part_order,type:int,default:0,notnull," json:"part_order"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
Priority int32 `bun:"priority,type:int,default:0,notnull," json:"priority"`
|
||||
RelPartID *ModelPublicAgentParts `bun:"rel:has-one,join:part_id=id" json:"relpartid,omitempty"` // Has one ModelPublicAgentParts
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAgentPersonaParts
|
||||
@@ -40,7 +40,7 @@ func (m ModelPublicAgentPersonaParts) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentPersonaParts) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,17 +3,18 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentPersonaSkills struct {
|
||||
bun.BaseModel `bun:"table:public.agent_persona_skills,alias:agent_persona_skills"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Override bool `bun:"override,type:boolean,default:false,notnull," json:"override"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAgentPersonaSkills
|
||||
@@ -38,7 +39,7 @@ func (m ModelPublicAgentPersonaSkills) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentPersonaSkills) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,17 +3,17 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentPersonaTraits struct {
|
||||
bun.BaseModel `bun:"table:public.agent_persona_traits,alias:agent_persona_traits"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
TraitID int64 `bun:"trait_id,type:bigint,notnull," json:"trait_id"`
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
RelTraitID *ModelPublicAgentTraits `bun:"rel:has-one,join:trait_id=id" json:"reltraitid,omitempty"` // Has one ModelPublicAgentTraits
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
TraitID int64 `bun:"trait_id,type:bigint,notnull," json:"trait_id"`
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
RelTraitID *ModelPublicAgentTraits `bun:"rel:has-one,join:trait_id=id" json:"reltraitid,omitempty"` // Has one ModelPublicAgentTraits
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAgentPersonaTraits
|
||||
@@ -38,7 +38,7 @@ func (m ModelPublicAgentPersonaTraits) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentPersonaTraits) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,26 +3,29 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentPersonas struct {
|
||||
bun.BaseModel `bun:"table:public.agent_personas,alias:agent_personas"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CompiledAt resolvespec_common.SqlTimeStamp `bun:"compiled_at,type:timestamptz,nullzero," json:"compiled_at"`
|
||||
CompiledDetail resolvespec_common.SqlString `bun:"compiled_detail,type:text,default:'',notnull," json:"compiled_detail"`
|
||||
CompiledSummary resolvespec_common.SqlString `bun:"compiled_summary,type:text,default:'',notnull," json:"compiled_summary"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Detail resolvespec_common.SqlString `bun:"detail,type:text,default:'',notnull," json:"detail"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CompiledAt sql_types.SqlTimeStamp `bun:"compiled_at,type:timestamptz,nullzero," json:"compiled_at"`
|
||||
CompiledDetail sql_types.SqlString `bun:"compiled_detail,type:text,default:'',notnull," json:"compiled_detail"`
|
||||
CompiledSummary sql_types.SqlString `bun:"compiled_summary,type:text,default:'',notnull," json:"compiled_summary"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Detail sql_types.SqlString `bun:"detail,type:text,default:'',notnull," json:"detail"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelPersonaIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
|
||||
RelPersonaIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
|
||||
RelPersonaIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas
|
||||
RelPersonaIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails
|
||||
RelPersonaIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
|
||||
RelPersonaIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||
@@ -50,7 +53,7 @@ func (m ModelPublicAgentPersonas) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentPersonas) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,28 +3,31 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentSkills struct {
|
||||
bun.BaseModel `bun:"table:public.agent_skills,alias:agent_skills"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
DomainTags resolvespec_common.SqlStringArray `bun:"domain_tags,type:text,default:'{}',notnull," json:"domain_tags"`
|
||||
FrameworkTags resolvespec_common.SqlStringArray `bun:"framework_tags,type:text,default:'{}',notnull," json:"framework_tags"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
LanguageTags resolvespec_common.SqlStringArray `bun:"language_tags,type:text,default:'{}',notnull," json:"language_tags"`
|
||||
LibraryTags resolvespec_common.SqlStringArray `bun:"library_tags,type:text,default:'{}',notnull," json:"library_tags"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelSkillIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
|
||||
RelRelatedSkillIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_skill_id" json:"relrelatedskillidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||
RelSkillIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
|
||||
RelSkillIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
DomainTags sql_types.SqlStringArray `bun:"domain_tags,type:text[],default:'{}',notnull," json:"domain_tags"`
|
||||
FrameworkTags sql_types.SqlStringArray `bun:"framework_tags,type:text[],default:'{}',notnull," json:"framework_tags"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
LanguageTags sql_types.SqlStringArray `bun:"language_tags,type:text[],default:'{}',notnull," json:"language_tags"`
|
||||
LibraryTags sql_types.SqlStringArray `bun:"library_tags,type:text[],default:'{}',notnull," json:"library_tags"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,array,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelSkillIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
|
||||
RelRelatedSkillIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_skill_id" json:"relrelatedskillidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||
RelSkillIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
|
||||
RelSkillIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAgentSkills
|
||||
@@ -49,7 +52,7 @@ func (m ModelPublicAgentSkills) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentSkills) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,22 +3,24 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAgentTraits struct {
|
||||
bun.BaseModel `bun:"table:public.agent_traits,alias:agent_traits"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Instruction resolvespec_common.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||
TraitType resolvespec_common.SqlString `bun:"trait_type,type:text,notnull," json:"trait_type"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTraitIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=trait_id" json:"reltraitidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Instruction sql_types.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
TraitType sql_types.SqlString `bun:"trait_type,type:text,notnull," json:"trait_type"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelTraitIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=trait_id" json:"reltraitidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAgentTraits
|
||||
@@ -43,7 +45,7 @@ func (m ModelPublicAgentTraits) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAgentTraits) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAPIKeyAssignments struct {
|
||||
bun.BaseModel `bun:"table:public.api_key_assignments,alias:api_key_assignments"`
|
||||
KeyID sql_types.SqlString `bun:"key_id,type:text,pk," json:"key_id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Enabled bool `bun:"enabled,type:boolean,default:true,notnull," json:"enabled"`
|
||||
Source sql_types.SqlString `bun:"source,type:text,notnull," json:"source"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,notnull," json:"tenant_id"`
|
||||
UserID sql_types.SqlString `bun:"user_id,type:text,nullzero," json:"user_id"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelUserID *ModelPublicTenantUsers `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicTenantUsers
|
||||
RelKeyIDPublicManagedAPIKeys []*ModelPublicManagedAPIKeys `bun:"rel:has-many,join:key_id=key_id" json:"relkeyidpublicmanagedapikeys,omitempty"` // Has many ModelPublicManagedAPIKeys
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAPIKeyAssignments
|
||||
func (m ModelPublicAPIKeyAssignments) TableName() string {
|
||||
return "public.api_key_assignments"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicAPIKeyAssignments
|
||||
func (m ModelPublicAPIKeyAssignments) TableNameOnly() string {
|
||||
return "api_key_assignments"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicAPIKeyAssignments
|
||||
func (m ModelPublicAPIKeyAssignments) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicAPIKeyAssignments) GetID() string {
|
||||
return m.KeyID.String()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAPIKeyAssignments) GetIDStr() string {
|
||||
return m.KeyID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicAPIKeyAssignments) SetID(newid string) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicAPIKeyAssignments) UpdateID(newid string) {
|
||||
m.KeyID.FromString(newid)
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicAPIKeyAssignments) GetIDName() string {
|
||||
return "key_id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicAPIKeyAssignments) GetPrefix() string {
|
||||
return "AKA"
|
||||
}
|
||||
@@ -3,17 +3,17 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicArcStageParts struct {
|
||||
bun.BaseModel `bun:"table:public.arc_stage_parts,alias:arc_stage_parts"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_id"`
|
||||
StageID int64 `bun:"stage_id,type:bigint,notnull," json:"stage_id"`
|
||||
RelPartID *ModelPublicAgentParts `bun:"rel:has-one,join:part_id=id" json:"relpartid,omitempty"` // Has one ModelPublicAgentParts
|
||||
RelStageID *ModelPublicArcStages `bun:"rel:has-one,join:stage_id=id" json:"relstageid,omitempty"` // Has one ModelPublicArcStages
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_id"`
|
||||
StageID int64 `bun:"stage_id,type:bigint,notnull," json:"stage_id"`
|
||||
RelPartID *ModelPublicAgentParts `bun:"rel:has-one,join:part_id=id" json:"relpartid,omitempty"` // Has one ModelPublicAgentParts
|
||||
RelStageID *ModelPublicArcStages `bun:"rel:has-one,join:stage_id=id" json:"relstageid,omitempty"` // Has one ModelPublicArcStages
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicArcStageParts
|
||||
@@ -38,7 +38,7 @@ func (m ModelPublicArcStageParts) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicArcStageParts) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,22 +3,22 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicArcStages struct {
|
||||
bun.BaseModel `bun:"table:public.arc_stages,alias:arc_stages"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
|
||||
Condition resolvespec_common.SqlString `bun:"condition,type:text,default:'',notnull," json:"condition"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
StageOrder int32 `bun:"stage_order,type:int,default:0,notnull," json:"stage_order"`
|
||||
RelArcID *ModelPublicCharacterArcs `bun:"rel:has-one,join:arc_id=id" json:"relarcid,omitempty"` // Has one ModelPublicCharacterArcs
|
||||
RelStageIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=stage_id" json:"relstageidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
|
||||
RelCurrentStageIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=current_stage_id" json:"relcurrentstageidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
|
||||
Condition sql_types.SqlString `bun:"condition,type:text,default:'',notnull," json:"condition"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
StageOrder int32 `bun:"stage_order,type:int,default:0,notnull," json:"stage_order"`
|
||||
RelArcID *ModelPublicCharacterArcs `bun:"rel:has-one,join:arc_id=id" json:"relarcid,omitempty"` // Has one ModelPublicCharacterArcs
|
||||
RelStageIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=stage_id" json:"relstageidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
|
||||
RelCurrentStageIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=current_stage_id" json:"relcurrentstageidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicArcStages
|
||||
@@ -43,7 +43,7 @@ func (m ModelPublicArcStages) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicArcStages) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,20 +3,22 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicCharacterArcs struct {
|
||||
bun.BaseModel `bun:"table:public.character_arcs,alias:character_arcs"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,default:'',notnull," json:"summary"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelArcIDPublicArcStages []*ModelPublicArcStages `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicarcstages,omitempty"` // Has many ModelPublicArcStages
|
||||
RelArcIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,default:'',notnull," json:"summary"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelArcIDPublicArcStages []*ModelPublicArcStages `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicarcstages,omitempty"` // Has many ModelPublicArcStages
|
||||
RelArcIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicCharacterArcs
|
||||
@@ -41,7 +43,7 @@ func (m ModelPublicCharacterArcs) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicCharacterArcs) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,25 +3,27 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicChatHistories struct {
|
||||
bun.BaseModel `bun:"table:public.chat_histories,alias:chat_histories"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
AgentID resolvespec_common.SqlString `bun:"agent_id,type:text,nullzero," json:"agent_id"`
|
||||
Channel resolvespec_common.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Messages resolvespec_common.SqlJSONB `bun:"messages,type:jsonb,default:'',notnull," json:"messages"`
|
||||
Metadata resolvespec_common.SqlJSONB `bun:"metadata,type:jsonb,default:'{}',notnull," json:"metadata"`
|
||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
SessionID resolvespec_common.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
|
||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,nullzero," json:"summary"`
|
||||
Title resolvespec_common.SqlString `bun:"title,type:text,nullzero," json:"title"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
AgentID sql_types.SqlString `bun:"agent_id,type:text,nullzero," json:"agent_id"`
|
||||
Channel sql_types.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Messages sql_types.SqlJSONB `bun:"messages,type:jsonb,default:'[]',notnull," json:"messages"`
|
||||
Metadata sql_types.SqlJSONB `bun:"metadata,type:jsonb,default:'{}',notnull," json:"metadata"`
|
||||
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
SessionID sql_types.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,nullzero," json:"summary"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
Title sql_types.SqlString `bun:"title,type:text,nullzero," json:"title"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicChatHistories
|
||||
@@ -46,7 +48,7 @@ func (m ModelPublicChatHistories) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicChatHistories) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,21 +3,21 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicEmbeddings struct {
|
||||
bun.BaseModel `bun:"table:public.embeddings,alias:embeddings"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||
Dim int32 `bun:"dim,type:int,notnull," json:"dim"`
|
||||
Embedding resolvespec_common.SqlVector `bun:"embedding,type:vector,notnull," json:"embedding"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Model resolvespec_common.SqlString `bun:"model,type:text,notnull,unique:uidx_embeddings_thought_id_model," json:"model"`
|
||||
ThoughtID int64 `bun:"thought_id,type:bigint,notnull,unique:uidx_embeddings_thought_id_model," json:"thought_id"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"`
|
||||
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||
Dim int32 `bun:"dim,type:int,notnull," json:"dim"`
|
||||
Embedding sql_types.SqlVector `bun:"embedding,type:vector,notnull," json:"embedding"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Model sql_types.SqlString `bun:"model,type:text,notnull,unique:uidx_embeddings_thought_id_model," json:"model"`
|
||||
ThoughtID int64 `bun:"thought_id,type:bigint,notnull,unique:uidx_embeddings_thought_id_model," json:"thought_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"`
|
||||
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicEmbeddings
|
||||
@@ -42,7 +42,7 @@ func (m ModelPublicEmbeddings) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicEmbeddings) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,39 +3,42 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicLearnings struct {
|
||||
bun.BaseModel `bun:"table:public.learnings,alias:learnings"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ActionRequired bool `bun:"action_required,type:boolean,default:false,notnull," json:"action_required"`
|
||||
Area resolvespec_common.SqlString `bun:"area,type:text,default:'other',notnull," json:"area"`
|
||||
Category resolvespec_common.SqlString `bun:"category,type:text,default:'insight',notnull," json:"category"`
|
||||
Confidence resolvespec_common.SqlString `bun:"confidence,type:text,default:'hypothesis',notnull," json:"confidence"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Details resolvespec_common.SqlString `bun:"details,type:text,default:'',notnull," json:"details"`
|
||||
DuplicateOfLearningID resolvespec_common.SqlInt64 `bun:"duplicate_of_learning_id,type:bigint,nullzero," json:"duplicate_of_learning_id"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Priority resolvespec_common.SqlString `bun:"priority,type:text,default:'medium',notnull," json:"priority"`
|
||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
RelatedSkillID resolvespec_common.SqlInt64 `bun:"related_skill_id,type:bigint,nullzero," json:"related_skill_id"`
|
||||
RelatedThoughtID resolvespec_common.SqlInt64 `bun:"related_thought_id,type:bigint,nullzero," json:"related_thought_id"`
|
||||
ReviewedAt resolvespec_common.SqlTimeStamp `bun:"reviewed_at,type:timestamptz,nullzero," json:"reviewed_at"`
|
||||
ReviewedBy resolvespec_common.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
||||
SourceRef resolvespec_common.SqlString `bun:"source_ref,type:text,nullzero," json:"source_ref"`
|
||||
SourceType resolvespec_common.SqlString `bun:"source_type,type:text,nullzero," json:"source_type"`
|
||||
Status resolvespec_common.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
|
||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
SupersedesLearningID resolvespec_common.SqlInt64 `bun:"supersedes_learning_id,type:bigint,nullzero," json:"supersedes_learning_id"`
|
||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelRelatedSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:related_skill_id=id" json:"relrelatedskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
RelRelatedThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:related_thought_id=id" json:"relrelatedthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
RelSupersedesLearningID *ModelPublicLearnings `bun:"rel:has-one,join:supersedes_learning_id=id" json:"relsupersedeslearningid,omitempty"` // Has one ModelPublicLearnings
|
||||
bun.BaseModel `bun:"table:public.learnings,alias:learnings"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ActionRequired bool `bun:"action_required,type:boolean,default:false,notnull," json:"action_required"`
|
||||
Area sql_types.SqlString `bun:"area,type:text,default:'other',notnull," json:"area"`
|
||||
Category sql_types.SqlString `bun:"category,type:text,default:'insight',notnull," json:"category"`
|
||||
Confidence sql_types.SqlString `bun:"confidence,type:text,default:'hypothesis',notnull," json:"confidence"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Details sql_types.SqlString `bun:"details,type:text,default:'',notnull," json:"details"`
|
||||
DuplicateOfLearningID sql_types.SqlInt64 `bun:"duplicate_of_learning_id,type:bigint,nullzero," json:"duplicate_of_learning_id"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Priority sql_types.SqlString `bun:"priority,type:text,default:'medium',notnull," json:"priority"`
|
||||
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
RelatedSkillID sql_types.SqlInt64 `bun:"related_skill_id,type:bigint,nullzero," json:"related_skill_id"`
|
||||
RelatedThoughtID sql_types.SqlInt64 `bun:"related_thought_id,type:bigint,nullzero," json:"related_thought_id"`
|
||||
ReviewedAt sql_types.SqlTimeStamp `bun:"reviewed_at,type:timestamptz,nullzero," json:"reviewed_at"`
|
||||
ReviewedBy sql_types.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
||||
SourceRef sql_types.SqlString `bun:"source_ref,type:text,nullzero," json:"source_ref"`
|
||||
SourceType sql_types.SqlString `bun:"source_type,type:text,nullzero," json:"source_type"`
|
||||
Status sql_types.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
|
||||
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||
SupersedesLearningID sql_types.SqlInt64 `bun:"supersedes_learning_id,type:bigint,nullzero," json:"supersedes_learning_id"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelRelatedSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:related_skill_id=id" json:"relrelatedskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
RelRelatedThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:related_thought_id=id" json:"relrelatedthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
RelSupersedesLearningID *ModelPublicLearnings `bun:"rel:has-one,join:supersedes_learning_id=id" json:"relsupersedeslearningid,omitempty"` // Has one ModelPublicLearnings
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelLearningIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=learning_id" json:"rellearningidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicLearnings
|
||||
@@ -60,7 +63,7 @@ func (m ModelPublicLearnings) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicLearnings) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicManagedAPIKeys struct {
|
||||
bun.BaseModel `bun:"table:public.managed_api_keys,alias:managed_api_keys"`
|
||||
KeyID sql_types.SqlString `bun:"key_id,type:text,pk," json:"key_id"`
|
||||
SecretHash sql_types.SqlString `bun:"secret_hash,type:text,notnull," json:"secret_hash"`
|
||||
RelKeyID *ModelPublicAPIKeyAssignments `bun:"rel:has-one,join:key_id=key_id" json:"relkeyid,omitempty"` // Has one ModelPublicAPIKeyAssignments
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicManagedAPIKeys
|
||||
func (m ModelPublicManagedAPIKeys) TableName() string {
|
||||
return "public.managed_api_keys"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicManagedAPIKeys
|
||||
func (m ModelPublicManagedAPIKeys) TableNameOnly() string {
|
||||
return "managed_api_keys"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicManagedAPIKeys
|
||||
func (m ModelPublicManagedAPIKeys) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicManagedAPIKeys) GetID() string {
|
||||
return m.KeyID.String()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicManagedAPIKeys) GetIDStr() string {
|
||||
return m.KeyID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicManagedAPIKeys) SetID(newid string) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicManagedAPIKeys) UpdateID(newid string) {
|
||||
m.KeyID.FromString(newid)
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicManagedAPIKeys) GetIDName() string {
|
||||
return "key_id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicManagedAPIKeys) GetPrefix() string {
|
||||
return "MAK"
|
||||
}
|
||||
@@ -3,17 +3,17 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicOauthClients struct {
|
||||
bun.BaseModel `bun:"table:public.oauth_clients,alias:oauth_clients"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ClientID resolvespec_common.SqlString `bun:"client_id,type:text,notnull," json:"client_id"`
|
||||
ClientName resolvespec_common.SqlString `bun:"client_name,type:text,default:'',notnull," json:"client_name"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
RedirectUris resolvespec_common.SqlStringArray `bun:"redirect_uris,type:text,default:'{}',notnull," json:"redirect_uris"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ClientID sql_types.SqlString `bun:"client_id,type:text,notnull," json:"client_id"`
|
||||
ClientName sql_types.SqlString `bun:"client_name,type:text,default:'',notnull," json:"client_name"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
RedirectUris sql_types.SqlStringArray `bun:"redirect_uris,type:text[],default:'{}',notnull," json:"redirect_uris"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicOauthClients
|
||||
@@ -38,7 +38,7 @@ func (m ModelPublicOauthClients) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicOauthClients) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,20 +3,20 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicPersonaArc struct {
|
||||
bun.BaseModel `bun:"table:public.persona_arc,alias:persona_arc"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,pk," json:"persona_id"`
|
||||
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
|
||||
CurrentStageID int64 `bun:"current_stage_id,type:bigint,notnull," json:"current_stage_id"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelArcID *ModelPublicCharacterArcs `bun:"rel:has-one,join:arc_id=id" json:"relarcid,omitempty"` // Has one ModelPublicCharacterArcs
|
||||
RelCurrentStageID *ModelPublicArcStages `bun:"rel:has-one,join:current_stage_id=id" json:"relcurrentstageid,omitempty"` // Has one ModelPublicArcStages
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
|
||||
CurrentStageID int64 `bun:"current_stage_id,type:bigint,notnull," json:"current_stage_id"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelArcID *ModelPublicCharacterArcs `bun:"rel:has-one,join:arc_id=id" json:"relarcid,omitempty"` // Has one ModelPublicCharacterArcs
|
||||
RelCurrentStageID *ModelPublicArcStages `bun:"rel:has-one,join:current_stage_id=id" json:"relcurrentstageid,omitempty"` // Has one ModelPublicArcStages
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicPersonaArc
|
||||
@@ -41,7 +41,7 @@ func (m ModelPublicPersonaArc) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicPersonaArc) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,18 +3,18 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicPlanDependencies struct {
|
||||
bun.BaseModel `bun:"table:public.plan_dependencies,alias:plan_dependencies"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
DependsOnPlanID int64 `bun:"depends_on_plan_id,type:bigint,notnull,unique:uidx_plan_dependencies_plan_id_depends_on_plan_id," json:"depends_on_plan_id"`
|
||||
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_dependencies_plan_id_depends_on_plan_id," json:"plan_id"`
|
||||
RelDependsOnPlanID *ModelPublicPlans `bun:"rel:has-one,join:depends_on_plan_id=id" json:"reldependsonplanid,omitempty"` // Has one ModelPublicPlans
|
||||
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
DependsOnPlanID int64 `bun:"depends_on_plan_id,type:bigint,notnull,unique:uidx_plan_dependencies_plan_id_depends_on_plan_id," json:"depends_on_plan_id"`
|
||||
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_dependencies_plan_id_depends_on_plan_id," json:"plan_id"`
|
||||
RelDependsOnPlanID *ModelPublicPlans `bun:"rel:has-one,join:depends_on_plan_id=id" json:"reldependsonplanid,omitempty"` // Has one ModelPublicPlans
|
||||
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicPlanDependencies
|
||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanDependencies) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicPlanDependencies) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,18 +3,18 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicPlanGuardrails struct {
|
||||
bun.BaseModel `bun:"table:public.plan_guardrails,alias:plan_guardrails"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull,unique:uidx_plan_guardrails_plan_id_guardrail_id," json:"guardrail_id"`
|
||||
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_guardrails_plan_id_guardrail_id," json:"plan_id"`
|
||||
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
|
||||
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull,unique:uidx_plan_guardrails_plan_id_guardrail_id," json:"guardrail_id"`
|
||||
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_guardrails_plan_id_guardrail_id," json:"plan_id"`
|
||||
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
|
||||
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicPlanGuardrails
|
||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanGuardrails) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicPlanGuardrails) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,18 +3,18 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicPlanRelatedPlans struct {
|
||||
bun.BaseModel `bun:"table:public.plan_related_plans,alias:plan_related_plans"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
PlanAID int64 `bun:"plan_a_id,type:bigint,notnull,unique:uidx_plan_related_plans_plan_a_id_plan_b_id," json:"plan_a_id"`
|
||||
PlanBID int64 `bun:"plan_b_id,type:bigint,notnull,unique:uidx_plan_related_plans_plan_a_id_plan_b_id," json:"plan_b_id"`
|
||||
RelPlanAID *ModelPublicPlans `bun:"rel:has-one,join:plan_a_id=id" json:"relplanaid,omitempty"` // Has one ModelPublicPlans
|
||||
RelPlanBID *ModelPublicPlans `bun:"rel:has-one,join:plan_b_id=id" json:"relplanbid,omitempty"` // Has one ModelPublicPlans
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
PlanAID int64 `bun:"plan_a_id,type:bigint,notnull,unique:uidx_plan_related_plans_plan_a_id_plan_b_id," json:"plan_a_id"`
|
||||
PlanBID int64 `bun:"plan_b_id,type:bigint,notnull,unique:uidx_plan_related_plans_plan_a_id_plan_b_id," json:"plan_b_id"`
|
||||
RelPlanAID *ModelPublicPlans `bun:"rel:has-one,join:plan_a_id=id" json:"relplanaid,omitempty"` // Has one ModelPublicPlans
|
||||
RelPlanBID *ModelPublicPlans `bun:"rel:has-one,join:plan_b_id=id" json:"relplanbid,omitempty"` // Has one ModelPublicPlans
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicPlanRelatedPlans
|
||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanRelatedPlans) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicPlanRelatedPlans) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,18 +3,18 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicPlanSkills struct {
|
||||
bun.BaseModel `bun:"table:public.plan_skills,alias:plan_skills"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_skills_plan_id_skill_id," json:"plan_id"`
|
||||
SkillID int64 `bun:"skill_id,type:bigint,notnull,unique:uidx_plan_skills_plan_id_skill_id," json:"skill_id"`
|
||||
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_skills_plan_id_skill_id," json:"plan_id"`
|
||||
SkillID int64 `bun:"skill_id,type:bigint,notnull,unique:uidx_plan_skills_plan_id_skill_id," json:"skill_id"`
|
||||
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicPlanSkills
|
||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanSkills) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicPlanSkills) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,36 +3,38 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicPlans struct {
|
||||
bun.BaseModel `bun:"table:public.plans,alias:plans"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CompletedAt resolvespec_common.SqlTimeStamp `bun:"completed_at,type:timestamptz,nullzero," json:"completed_at"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
DueDate resolvespec_common.SqlTimeStamp `bun:"due_date,type:timestamptz,nullzero," json:"due_date"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
LastReviewedAt resolvespec_common.SqlTimeStamp `bun:"last_reviewed_at,type:timestamptz,nullzero," json:"last_reviewed_at"`
|
||||
Owner resolvespec_common.SqlString `bun:"owner,type:text,nullzero," json:"owner"`
|
||||
Priority resolvespec_common.SqlString `bun:"priority,type:text,default:'medium',notnull," json:"priority"` // low, medium, high, critical
|
||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
ReviewedBy resolvespec_common.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
||||
Status resolvespec_common.SqlString `bun:"status,type:text,default:'draft',notnull," json:"status"` // draft, active, blocked, completed, cancelled, superseded
|
||||
SupersedesPlanID resolvespec_common.SqlInt64 `bun:"supersedes_plan_id,type:bigint,nullzero," json:"supersedes_plan_id"`
|
||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
||||
Title resolvespec_common.SqlString `bun:"title,type:text,notnull," json:"title"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelSupersedesPlanID *ModelPublicPlans `bun:"rel:has-one,join:supersedes_plan_id=id" json:"relsupersedesplanid,omitempty"` // Has one ModelPublicPlans
|
||||
RelDependsOnPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=depends_on_plan_id" json:"reldependsonplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||
RelPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||
RelPlanAIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_a_id" json:"relplanaidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||
RelPlanBIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_b_id" json:"relplanbidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||
RelPlanIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
|
||||
RelPlanIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CompletedAt sql_types.SqlTimeStamp `bun:"completed_at,type:timestamptz,nullzero," json:"completed_at"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||
DueDate sql_types.SqlTimeStamp `bun:"due_date,type:timestamptz,nullzero," json:"due_date"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
LastReviewedAt sql_types.SqlTimeStamp `bun:"last_reviewed_at,type:timestamptz,nullzero," json:"last_reviewed_at"`
|
||||
Owner sql_types.SqlString `bun:"owner,type:text,nullzero," json:"owner"`
|
||||
Priority sql_types.SqlString `bun:"priority,type:text,default:'medium',notnull," json:"priority"` // low, medium, high, critical
|
||||
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
ReviewedBy sql_types.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
||||
Status sql_types.SqlString `bun:"status,type:text,default:'draft',notnull," json:"status"` // draft, active, blocked, completed, cancelled, superseded
|
||||
SupersedesPlanID sql_types.SqlInt64 `bun:"supersedes_plan_id,type:bigint,nullzero," json:"supersedes_plan_id"`
|
||||
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
Title sql_types.SqlString `bun:"title,type:text,notnull," json:"title"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelSupersedesPlanID *ModelPublicPlans `bun:"rel:has-one,join:supersedes_plan_id=id" json:"relsupersedesplanid,omitempty"` // Has one ModelPublicPlans
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelDependsOnPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=depends_on_plan_id" json:"reldependsonplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||
RelPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||
RelPlanAIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_a_id" json:"relplanaidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||
RelPlanBIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_b_id" json:"relplanbidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||
RelPlanIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
|
||||
RelPlanIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicPlans
|
||||
@@ -57,7 +59,7 @@ func (m ModelPublicPlans) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicPlans) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,18 +3,18 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicProjectGuardrails struct {
|
||||
bun.BaseModel `bun:"table:public.project_guardrails,alias:project_guardrails"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull," json:"guardrail_id"`
|
||||
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
||||
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull," json:"guardrail_id"`
|
||||
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
||||
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicProjectGuardrails
|
||||
@@ -39,7 +39,7 @@ func (m ModelPublicProjectGuardrails) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicProjectGuardrails) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicProjectPersonas struct {
|
||||
bun.BaseModel `bun:"table:public.project_personas,alias:project_personas"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
IsDefault bool `bun:"is_default,type:boolean,default:false,notnull," json:"is_default"`
|
||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicProjectPersonas
|
||||
func (m ModelPublicProjectPersonas) TableName() string {
|
||||
return "public.project_personas"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicProjectPersonas
|
||||
func (m ModelPublicProjectPersonas) TableNameOnly() string {
|
||||
return "project_personas"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicProjectPersonas
|
||||
func (m ModelPublicProjectPersonas) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicProjectPersonas) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicProjectPersonas) GetIDStr() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicProjectPersonas) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicProjectPersonas) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicProjectPersonas) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicProjectPersonas) GetPrefix() string {
|
||||
return "PPR"
|
||||
}
|
||||
@@ -3,18 +3,19 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicProjectSkills struct {
|
||||
bun.BaseModel `bun:"table:public.project_skills,alias:project_skills"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
||||
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Override bool `bun:"override,type:boolean,default:false,notnull," json:"override"`
|
||||
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
||||
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicProjectSkills
|
||||
@@ -39,7 +40,7 @@ func (m ModelPublicProjectSkills) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicProjectSkills) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,19 +3,21 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicProjects struct {
|
||||
bun.BaseModel `bun:"table:public.projects,alias:projects"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,nullzero," json:"description"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
LastActiveAt resolvespec_common.SqlTimeStamp `bun:"last_active_at,type:timestamptz,default:now(),nullzero," json:"last_active_at"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
ThoughtCount resolvespec_common.SqlInt64 `bun:"thought_count,scanonly" json:"thought_count"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||
Description sql_types.SqlString `bun:"description,type:text,nullzero," json:"description"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
LastActiveAt sql_types.SqlTimeStamp `bun:"last_active_at,type:timestamptz,default:now(),nullzero," json:"last_active_at"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull,unique:uidx_projects_tenant_id_name," json:"name"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero,unique:uidx_projects_tenant_id_name," json:"tenant_id"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelProjectIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas
|
||||
RelProjectIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicthoughts,omitempty"` // Has many ModelPublicThoughts
|
||||
RelProjectIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
||||
RelProjectIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
|
||||
@@ -47,7 +49,7 @@ func (m ModelPublicProjects) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicProjects) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,27 +3,29 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicStoredFiles struct {
|
||||
bun.BaseModel `bun:"table:public.stored_files,alias:stored_files"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Content []byte `bun:"content,type:bytea,notnull," json:"content"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Encoding resolvespec_common.SqlString `bun:"encoding,type:text,default:'base64',notnull," json:"encoding"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Kind resolvespec_common.SqlString `bun:"kind,type:text,default:'file',notnull," json:"kind"`
|
||||
MediaType resolvespec_common.SqlString `bun:"media_type,type:text,notnull," json:"media_type"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
Sha256 resolvespec_common.SqlString `bun:"sha256,type:text,notnull," json:"sha256"`
|
||||
SizeBytes int64 `bun:"size_bytes,type:bigint,notnull," json:"size_bytes"`
|
||||
ThoughtID resolvespec_common.SqlInt64 `bun:"thought_id,type:bigint,nullzero," json:"thought_id"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
Content []byte `bun:"content,type:bytea,notnull," json:"content"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Encoding sql_types.SqlString `bun:"encoding,type:text,default:'base64',notnull," json:"encoding"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Kind sql_types.SqlString `bun:"kind,type:text,default:'file',notnull," json:"kind"`
|
||||
MediaType sql_types.SqlString `bun:"media_type,type:text,notnull," json:"media_type"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
Sha256 sql_types.SqlString `bun:"sha256,type:text,notnull," json:"sha256"`
|
||||
SizeBytes int64 `bun:"size_bytes,type:bigint,notnull," json:"size_bytes"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
ThoughtID sql_types.SqlInt64 `bun:"thought_id,type:bigint,nullzero," json:"thought_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicStoredFiles
|
||||
@@ -48,7 +50,7 @@ func (m ModelPublicStoredFiles) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicStoredFiles) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicTenantUsers struct {
|
||||
bun.BaseModel `bun:"table:public.tenant_users,alias:tenant_users"`
|
||||
ID sql_types.SqlString `bun:"id,type:text,pk," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Email sql_types.SqlString `bun:"email,type:text,nullzero,unique:uidx_tenant_users_tenant_id_email," json:"email"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,notnull,unique:uidx_tenant_users_tenant_id_email," json:"tenant_id"`
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelUserIDPublicAPIKeyAssignments []*ModelPublicAPIKeyAssignments `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicapikeyassignments,omitempty"` // Has many ModelPublicAPIKeyAssignments
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicTenantUsers
|
||||
func (m ModelPublicTenantUsers) TableName() string {
|
||||
return "public.tenant_users"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicTenantUsers
|
||||
func (m ModelPublicTenantUsers) TableNameOnly() string {
|
||||
return "tenant_users"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicTenantUsers
|
||||
func (m ModelPublicTenantUsers) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicTenantUsers) GetID() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicTenantUsers) GetIDStr() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicTenantUsers) SetID(newid string) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicTenantUsers) UpdateID(newid string) {
|
||||
m.ID.FromString(newid)
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicTenantUsers) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicTenantUsers) GetPrefix() string {
|
||||
return "TUE"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicTenants struct {
|
||||
bun.BaseModel `bun:"table:public.tenants,alias:tenants"`
|
||||
ID sql_types.SqlString `bun:"id,type:text,pk," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||
RelTenantIDPublicAgentPersonas []*ModelPublicAgentPersonas `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentpersonas,omitempty"` // Has many ModelPublicAgentPersonas
|
||||
RelTenantIDPublicAgentParts []*ModelPublicAgentParts `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentparts,omitempty"` // Has many ModelPublicAgentParts
|
||||
RelTenantIDPublicAgentTraits []*ModelPublicAgentTraits `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagenttraits,omitempty"` // Has many ModelPublicAgentTraits
|
||||
RelTenantIDPublicCharacterArcs []*ModelPublicCharacterArcs `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpubliccharacterarcs,omitempty"` // Has many ModelPublicCharacterArcs
|
||||
RelTenantIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicthoughts,omitempty"` // Has many ModelPublicThoughts
|
||||
RelTenantIDPublicProjects []*ModelPublicProjects `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicprojects,omitempty"` // Has many ModelPublicProjects
|
||||
RelTenantIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
||||
RelTenantIDPublicTenantUsers []*ModelPublicTenantUsers `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublictenantusers,omitempty"` // Has many ModelPublicTenantUsers
|
||||
RelTenantIDPublicAPIKeyAssignments []*ModelPublicAPIKeyAssignments `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicapikeyassignments,omitempty"` // Has many ModelPublicAPIKeyAssignments
|
||||
RelTenantIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
|
||||
RelTenantIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||
RelTenantIDPublicPlans []*ModelPublicPlans `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicplans,omitempty"` // Has many ModelPublicPlans
|
||||
RelTenantIDPublicAgentSkills []*ModelPublicAgentSkills `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentskills,omitempty"` // Has many ModelPublicAgentSkills
|
||||
RelTenantIDPublicAgentGuardrails []*ModelPublicAgentGuardrails `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentguardrails,omitempty"` // Has many ModelPublicAgentGuardrails
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicTenants
|
||||
func (m ModelPublicTenants) TableName() string {
|
||||
return "public.tenants"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicTenants
|
||||
func (m ModelPublicTenants) TableNameOnly() string {
|
||||
return "tenants"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicTenants
|
||||
func (m ModelPublicTenants) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicTenants) GetID() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicTenants) GetIDStr() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicTenants) SetID(newid string) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicTenants) UpdateID(newid string) {
|
||||
m.ID.FromString(newid)
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicTenants) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicTenants) GetPrefix() string {
|
||||
return "TEN"
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicThoughtLearningLinks struct {
|
||||
bun.BaseModel `bun:"table:public.thought_learning_links,alias:thought_learning_links"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
LearningID int64 `bun:"learning_id,type:bigint,notnull,unique:uidx_thought_learning_links_thought_id_learning_id," json:"learning_id"`
|
||||
Relation sql_types.SqlString `bun:"relation,type:text,default:'source',notnull," json:"relation"`
|
||||
ThoughtID int64 `bun:"thought_id,type:bigint,notnull,unique:uidx_thought_learning_links_thought_id_learning_id," json:"thought_id"`
|
||||
RelLearningID *ModelPublicLearnings `bun:"rel:has-one,join:learning_id=id" json:"rellearningid,omitempty"` // Has one ModelPublicLearnings
|
||||
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicThoughtLearningLinks
|
||||
func (m ModelPublicThoughtLearningLinks) TableName() string {
|
||||
return "public.thought_learning_links"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicThoughtLearningLinks
|
||||
func (m ModelPublicThoughtLearningLinks) TableNameOnly() string {
|
||||
return "thought_learning_links"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicThoughtLearningLinks
|
||||
func (m ModelPublicThoughtLearningLinks) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicThoughtLearningLinks) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicThoughtLearningLinks) GetIDStr() string {
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicThoughtLearningLinks) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicThoughtLearningLinks) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicThoughtLearningLinks) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicThoughtLearningLinks) GetPrefix() string {
|
||||
return "TLL"
|
||||
}
|
||||
@@ -3,19 +3,19 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicThoughtLinks struct {
|
||||
bun.BaseModel `bun:"table:public.thought_links,alias:thought_links"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||
FromID int64 `bun:"from_id,type:bigint,notnull," json:"from_id"`
|
||||
Relation resolvespec_common.SqlString `bun:"relation,type:text,notnull," json:"relation"`
|
||||
ToID int64 `bun:"to_id,type:bigint,notnull," json:"to_id"`
|
||||
RelFromID *ModelPublicThoughts `bun:"rel:has-one,join:from_id=id" json:"relfromid,omitempty"` // Has one ModelPublicThoughts
|
||||
RelToID *ModelPublicThoughts `bun:"rel:has-one,join:to_id=id" json:"reltoid,omitempty"` // Has one ModelPublicThoughts
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||
FromID int64 `bun:"from_id,type:bigint,notnull," json:"from_id"`
|
||||
Relation sql_types.SqlString `bun:"relation,type:text,notnull," json:"relation"`
|
||||
ToID int64 `bun:"to_id,type:bigint,notnull," json:"to_id"`
|
||||
RelFromID *ModelPublicThoughts `bun:"rel:has-one,join:from_id=id" json:"relfromid,omitempty"` // Has one ModelPublicThoughts
|
||||
RelToID *ModelPublicThoughts `bun:"rel:has-one,join:to_id=id" json:"reltoid,omitempty"` // Has one ModelPublicThoughts
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicThoughtLinks
|
||||
@@ -40,7 +40,7 @@ func (m ModelPublicThoughtLinks) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicThoughtLinks) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,26 +3,29 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicThoughts struct {
|
||||
bun.BaseModel `bun:"table:public.thoughts,alias:thoughts"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ArchivedAt resolvespec_common.SqlTimeStamp `bun:"archived_at,type:timestamptz,nullzero," json:"archived_at"`
|
||||
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Metadata resolvespec_common.SqlJSONB `bun:"metadata,type:jsonb,default:{}::jsonb,nullzero," json:"metadata"`
|
||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelFromIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=from_id" json:"relfromidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
||||
RelToIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=to_id" json:"reltoidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
||||
RelThoughtIDPublicEmbeddings []*ModelPublicEmbeddings `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicembeddings,omitempty"` // Has many ModelPublicEmbeddings
|
||||
RelThoughtIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
||||
RelRelatedThoughtIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_thought_id" json:"relrelatedthoughtidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||
bun.BaseModel `bun:"table:public.thoughts,alias:thoughts"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
ArchivedAt sql_types.SqlTimeStamp `bun:"archived_at,type:timestamptz,nullzero," json:"archived_at"`
|
||||
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||
Metadata sql_types.SqlJSONB `bun:"metadata,type:jsonb,default:{}::jsonb,nullzero," json:"metadata"`
|
||||
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"`
|
||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
|
||||
RelFromIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=from_id" json:"relfromidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
||||
RelToIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=to_id" json:"reltoidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
||||
RelThoughtIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks
|
||||
RelThoughtIDPublicEmbeddings []*ModelPublicEmbeddings `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicembeddings,omitempty"` // Has many ModelPublicEmbeddings
|
||||
RelThoughtIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
||||
RelRelatedThoughtIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_thought_id" json:"relrelatedthoughtidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicThoughts
|
||||
@@ -47,7 +50,7 @@ func (m ModelPublicThoughts) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicThoughts) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
@@ -3,17 +3,17 @@ package generatedmodels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicToolAnnotations struct {
|
||||
bun.BaseModel `bun:"table:public.tool_annotations,alias:tool_annotations"`
|
||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Notes resolvespec_common.SqlString `bun:"notes,type:text,default:'',notnull," json:"notes"`
|
||||
ToolName resolvespec_common.SqlString `bun:"tool_name,type:text,notnull," json:"tool_name"`
|
||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||
Notes sql_types.SqlString `bun:"notes,type:text,default:'',notnull," json:"notes"`
|
||||
ToolName sql_types.SqlString `bun:"tool_name,type:text,notnull," json:"tool_name"`
|
||||
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicToolAnnotations
|
||||
@@ -38,7 +38,7 @@ func (m ModelPublicToolAnnotations) GetID() int64 {
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicToolAnnotations) GetIDStr() string {
|
||||
return fmt.Sprintf("%v", m.ID)
|
||||
return m.ID.String()
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
|
||||
+102
-24
@@ -1,6 +1,7 @@
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -18,31 +19,35 @@ const (
|
||||
)
|
||||
|
||||
type ToolSet struct {
|
||||
Version *tools.VersionTool
|
||||
Capture *tools.CaptureTool
|
||||
Search *tools.SearchTool
|
||||
List *tools.ListTool
|
||||
Stats *tools.StatsTool
|
||||
Get *tools.GetTool
|
||||
Update *tools.UpdateTool
|
||||
Delete *tools.DeleteTool
|
||||
Archive *tools.ArchiveTool
|
||||
Projects *tools.ProjectsTool
|
||||
Context *tools.ContextTool
|
||||
Recall *tools.RecallTool
|
||||
Summarize *tools.SummarizeTool
|
||||
Links *tools.LinksTool
|
||||
Files *tools.FilesTool
|
||||
Backfill *tools.BackfillTool
|
||||
Reparse *tools.ReparseMetadataTool
|
||||
RetryMetadata *tools.RetryEnrichmentTool
|
||||
Version *tools.VersionTool
|
||||
Capture *tools.CaptureTool
|
||||
Search *tools.SearchTool
|
||||
List *tools.ListTool
|
||||
Stats *tools.StatsTool
|
||||
Get *tools.GetTool
|
||||
Update *tools.UpdateTool
|
||||
Delete *tools.DeleteTool
|
||||
Archive *tools.ArchiveTool
|
||||
DuplicateAudit *tools.DuplicateAuditTool
|
||||
Projects *tools.ProjectsTool
|
||||
Context *tools.ContextTool
|
||||
Recall *tools.RecallTool
|
||||
Summarize *tools.SummarizeTool
|
||||
Links *tools.LinksTool
|
||||
Files *tools.FilesTool
|
||||
Backfill *tools.BackfillTool
|
||||
Reparse *tools.ReparseMetadataTool
|
||||
RetryMetadata *tools.RetryEnrichmentTool
|
||||
//Maintenance *tools.MaintenanceTool
|
||||
Skills *tools.SkillsTool
|
||||
Personas *tools.AgentPersonasTool
|
||||
ChatHistory *tools.ChatHistoryTool
|
||||
Describe *tools.DescribeTool
|
||||
Learnings *tools.LearningsTool
|
||||
Plans *tools.PlansTool
|
||||
Skills *tools.SkillsTool
|
||||
Personas *tools.AgentPersonasTool
|
||||
ChatHistory *tools.ChatHistoryTool
|
||||
Describe *tools.DescribeTool
|
||||
Learnings *tools.LearningsTool
|
||||
Plans *tools.PlansTool
|
||||
ProjectPersonas *tools.ProjectPersonasTool
|
||||
WorldModel *tools.WorldModelTool
|
||||
ThoughtLearningLinks *tools.ThoughtLearningLinksTool
|
||||
}
|
||||
|
||||
// Handlers groups the HTTP handlers produced for an MCP server instance.
|
||||
@@ -86,7 +91,9 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
|
||||
registerSystemTools,
|
||||
registerThoughtTools,
|
||||
registerProjectTools,
|
||||
registerWorldModelTools,
|
||||
registerLearningTools,
|
||||
registerThoughtLearningLinkTools,
|
||||
registerPlanTools,
|
||||
registerFileTools,
|
||||
registerMaintenanceTools,
|
||||
@@ -123,6 +130,33 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func registerWorldModelTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||
server.AddResource(&mcp.Resource{
|
||||
Name: "world_model_intro",
|
||||
URI: "amcs://world-model/intro",
|
||||
MIMEType: "text/markdown",
|
||||
Description: "Mandatory startup instructions for discovering and loading the AMCS world model.",
|
||||
}, func(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{Contents: []*mcp.ResourceContents{{URI: req.Params.URI, MIMEType: "text/markdown", Text: string(amcsllm.WorldModelIntro)}}}, nil
|
||||
})
|
||||
if err := addTool(server, logger, &mcp.Tool{Name: "bootstrap_world_model", Description: "Validate and activate a project, then return its effective skills, guardrails, personas, and bounded recent context."}, toolSet.WorldModel.Bootstrap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{Name: "add_project_persona", Description: "Link a persona to a project and optionally make it the default."}, toolSet.ProjectPersonas.Add); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{Name: "remove_project_persona", Description: "Unlink a persona from a project."}, toolSet.ProjectPersonas.Remove); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{Name: "set_default_project_persona", Description: "Set a linked persona as the project's default persona."}, toolSet.ProjectPersonas.SetDefault); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{Name: "list_project_personas", Description: "List personas linked to a project, with the default marked."}, toolSet.ProjectPersonas.List); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildServerIcons returns icon definitions referencing the server's own /images/icon.png endpoint.
|
||||
// Returns nil when publicURL is empty so the icons field is omitted from the MCP identity.
|
||||
func buildServerIcons(publicURL string) []mcp.Icon {
|
||||
@@ -194,6 +228,12 @@ func registerThoughtTools(server *mcp.Server, logger *slog.Logger, toolSet ToolS
|
||||
}, toolSet.Archive.Handle); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "audit_duplicates",
|
||||
Description: "Dry-run duplicate audit for projects, thoughts, and metadata normalization candidates; performs no writes.",
|
||||
}, toolSet.DuplicateAudit.Handle); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "summarize_thoughts",
|
||||
Description: "LLM summary of a filtered set of thoughts.",
|
||||
@@ -277,6 +317,34 @@ func registerLearningTools(server *mcp.Server, logger *slog.Logger, toolSet Tool
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerThoughtLearningLinkTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "link_thought_learning",
|
||||
Description: "Create or update an explicit association between a raw thought and a curated learning.",
|
||||
}, toolSet.ThoughtLearningLinks.Link); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "unlink_thought_learning",
|
||||
Description: "Remove an explicit association between a thought and a learning.",
|
||||
}, toolSet.ThoughtLearningLinks.Unlink); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "get_thought_learnings",
|
||||
Description: "List curated learnings explicitly linked to a thought.",
|
||||
}, toolSet.ThoughtLearningLinks.GetThoughtLearnings); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "get_learning_thoughts",
|
||||
Description: "List raw thoughts explicitly linked to a learning.",
|
||||
}, toolSet.ThoughtLearningLinks.GetLearningThoughts); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerPlanTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||
if err := addTool(server, logger, &mcp.Tool{
|
||||
Name: "create_plan",
|
||||
@@ -703,6 +771,7 @@ func BuildToolCatalog() []tools.ToolEntry {
|
||||
{Name: "update_thought", Description: "Update thought content or merge metadata.", Category: "thoughts"},
|
||||
{Name: "delete_thought", Description: "Hard-delete a thought by id.", Category: "thoughts"},
|
||||
{Name: "archive_thought", Description: "Archive a thought so it is hidden from default search and listing.", Category: "thoughts"},
|
||||
{Name: "audit_duplicates", Description: "Dry-run duplicate audit for exact/normalized project names, thought content, and metadata value variants. Reports candidates and recommendations; performs no writes.", Category: "admin"},
|
||||
{Name: "summarize_thoughts", Description: "Produce an LLM prose summary of a filtered or searched set of thoughts.", Category: "thoughts"},
|
||||
{Name: "recall_context", Description: "Recall semantically relevant and recent context for prompt injection. Combines vector similarity with recency. Falls back to full-text search when no embeddings exist.", Category: "thoughts"},
|
||||
{Name: "link_thoughts", Description: "Create a typed relationship between two thoughts.", Category: "thoughts"},
|
||||
@@ -713,12 +782,21 @@ func BuildToolCatalog() []tools.ToolEntry {
|
||||
{Name: "list_projects", Description: "List projects and their current thought counts.", Category: "projects"},
|
||||
{Name: "set_active_project", Description: "Set the active project for the current MCP session. Requires a stateful MCP client that reuses the same session across calls. If your client does not preserve sessions, pass project explicitly to each tool instead.", Category: "projects"},
|
||||
{Name: "get_active_project", Description: "Return the active project for the current MCP session. If your client does not preserve MCP sessions, pass project explicitly to project-scoped tools instead of relying on this.", Category: "projects"},
|
||||
{Name: "bootstrap_world_model", Description: "Validate and activate a project, then return effective skills, guardrails, personas, and bounded recent context.", Category: "projects"},
|
||||
{Name: "add_project_persona", Description: "Link a persona to a project and optionally make it the default.", Category: "personas"},
|
||||
{Name: "remove_project_persona", Description: "Unlink a persona from a project.", Category: "personas"},
|
||||
{Name: "set_default_project_persona", Description: "Set a linked persona as the project's default persona.", Category: "personas"},
|
||||
{Name: "list_project_personas", Description: "List personas linked to a project, with the default marked.", Category: "personas"},
|
||||
{Name: "get_project_context", Description: "Get recent and semantic context for a project. Uses the explicit project when provided, otherwise the active MCP session project. Falls back to full-text search when no embeddings exist.", Category: "projects"},
|
||||
|
||||
// learnings
|
||||
{Name: "add_learning", Description: "Create a curated learning record distinct from raw thoughts.", Category: "projects"},
|
||||
{Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"},
|
||||
{Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"},
|
||||
{Name: "link_thought_learning", Description: "Create or update a lightweight explicit association between a raw thought and a curated learning.", Category: "projects"},
|
||||
{Name: "unlink_thought_learning", Description: "Remove a lightweight explicit association between a thought and a learning.", Category: "projects"},
|
||||
{Name: "get_thought_learnings", Description: "List curated learnings explicitly associated with a thought.", Category: "projects"},
|
||||
{Name: "get_learning_thoughts", Description: "List raw source thoughts explicitly associated with a learning.", Category: "projects"},
|
||||
|
||||
// plans
|
||||
{Name: "create_plan", Description: "Create a structured plan with status, priority, owner, due date, and optional project link.", Category: "plans"},
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -60,6 +61,26 @@ func TestNewListsStoredFileResourceTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewExposesWorldModelIntroResource(t *testing.T) {
|
||||
cs := newStreamableTestClient(t)
|
||||
|
||||
listed, err := cs.ListResources(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ListResources() error = %v", err)
|
||||
}
|
||||
if len(listed.Resources) != 1 || listed.Resources[0].URI != "amcs://world-model/intro" {
|
||||
t.Fatalf("ListResources() = %#v, want world model intro", listed.Resources)
|
||||
}
|
||||
|
||||
read, err := cs.ReadResource(context.Background(), &mcp.ReadResourceParams{URI: "amcs://world-model/intro"})
|
||||
if err != nil {
|
||||
t.Fatalf("ReadResource() error = %v", err)
|
||||
}
|
||||
if len(read.Contents) != 1 || !strings.Contains(read.Contents[0].Text, "bootstrap_world_model") {
|
||||
t.Fatalf("ReadResource() did not return bootstrap instructions: %#v", read.Contents)
|
||||
}
|
||||
}
|
||||
|
||||
func newStreamableTestClient(t *testing.T) *mcp.ClientSession {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -189,28 +189,31 @@ func TestStreamableHTTPReturnsStructuredToolErrors(t *testing.T) {
|
||||
|
||||
func streamableTestToolSet() ToolSet {
|
||||
return ToolSet{
|
||||
Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}),
|
||||
Capture: new(tools.CaptureTool),
|
||||
Search: new(tools.SearchTool),
|
||||
List: new(tools.ListTool),
|
||||
Stats: new(tools.StatsTool),
|
||||
Get: new(tools.GetTool),
|
||||
Update: new(tools.UpdateTool),
|
||||
Delete: new(tools.DeleteTool),
|
||||
Archive: new(tools.ArchiveTool),
|
||||
Projects: new(tools.ProjectsTool),
|
||||
Context: new(tools.ContextTool),
|
||||
Recall: new(tools.RecallTool),
|
||||
Summarize: new(tools.SummarizeTool),
|
||||
Links: new(tools.LinksTool),
|
||||
Files: new(tools.FilesTool),
|
||||
Backfill: new(tools.BackfillTool),
|
||||
Reparse: new(tools.ReparseMetadataTool),
|
||||
RetryMetadata: new(tools.RetryEnrichmentTool),
|
||||
Skills: new(tools.SkillsTool),
|
||||
ChatHistory: new(tools.ChatHistoryTool),
|
||||
Describe: new(tools.DescribeTool),
|
||||
Learnings: new(tools.LearningsTool),
|
||||
Plans: new(tools.PlansTool),
|
||||
Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}),
|
||||
Capture: new(tools.CaptureTool),
|
||||
Search: new(tools.SearchTool),
|
||||
List: new(tools.ListTool),
|
||||
Stats: new(tools.StatsTool),
|
||||
Get: new(tools.GetTool),
|
||||
Update: new(tools.UpdateTool),
|
||||
Delete: new(tools.DeleteTool),
|
||||
Archive: new(tools.ArchiveTool),
|
||||
Projects: new(tools.ProjectsTool),
|
||||
Context: new(tools.ContextTool),
|
||||
Recall: new(tools.RecallTool),
|
||||
Summarize: new(tools.SummarizeTool),
|
||||
Links: new(tools.LinksTool),
|
||||
Files: new(tools.FilesTool),
|
||||
Backfill: new(tools.BackfillTool),
|
||||
Reparse: new(tools.ReparseMetadataTool),
|
||||
RetryMetadata: new(tools.RetryEnrichmentTool),
|
||||
Skills: new(tools.SkillsTool),
|
||||
ChatHistory: new(tools.ChatHistoryTool),
|
||||
Describe: new(tools.DescribeTool),
|
||||
Learnings: new(tools.LearningsTool),
|
||||
Plans: new(tools.PlansTool),
|
||||
ProjectPersonas: new(tools.ProjectPersonasTool),
|
||||
WorldModel: new(tools.WorldModelTool),
|
||||
ThoughtLearningLinks: new(tools.ThoughtLearningLinksTool),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ func Normalize(in thoughttypes.ThoughtMetadata, capture config.CaptureConfig) th
|
||||
Type: normalizeType(in.Type),
|
||||
Source: normalizeSource(in.Source),
|
||||
Attachments: normalizeAttachments(in.Attachments),
|
||||
Webhook: normalizeWebhook(in.Webhook),
|
||||
MetadataStatus: normalizeMetadataStatus(in.MetadataStatus),
|
||||
MetadataUpdatedAt: strings.TrimSpace(in.MetadataUpdatedAt),
|
||||
MetadataLastAttemptedAt: strings.TrimSpace(in.MetadataLastAttemptedAt),
|
||||
@@ -201,10 +202,31 @@ func Merge(base, patch thoughttypes.ThoughtMetadata, capture config.CaptureConfi
|
||||
if len(patch.Attachments) > 0 {
|
||||
merged.Attachments = append(append([]thoughttypes.ThoughtAttachment{}, merged.Attachments...), patch.Attachments...)
|
||||
}
|
||||
if patch.Webhook != nil {
|
||||
merged.Webhook = patch.Webhook
|
||||
}
|
||||
|
||||
return Normalize(merged, capture)
|
||||
}
|
||||
|
||||
func normalizeWebhook(value *thoughttypes.WebhookMetadata) *thoughttypes.WebhookMetadata {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
out := &thoughttypes.WebhookMetadata{
|
||||
ReceivedAt: strings.TrimSpace(value.ReceivedAt),
|
||||
IDempotencyKey: strings.TrimSpace(value.IDempotencyKey),
|
||||
ExternalID: strings.TrimSpace(value.ExternalID),
|
||||
}
|
||||
if len(value.SourceMetadata) > 0 {
|
||||
out.SourceMetadata = value.SourceMetadata
|
||||
}
|
||||
if out.ReceivedAt == "" && out.IDempotencyKey == "" && out.ExternalID == "" && len(out.SourceMetadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeAttachments(values []thoughttypes.ThoughtAttachment) []thoughttypes.ThoughtAttachment {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
result := make([]thoughttypes.ThoughtAttachment, 0, len(values))
|
||||
|
||||
@@ -234,6 +234,7 @@ func (db *DB) GetPersona(ctx context.Context, name string, detail bool, override
|
||||
Name: s.Name,
|
||||
Description: s.Description,
|
||||
Tags: s.Tags,
|
||||
Override: s.Override,
|
||||
}
|
||||
if detail {
|
||||
entry.Content = s.Content
|
||||
@@ -341,6 +342,7 @@ func (db *DB) GetPersonaManifest(ctx context.Context, name string) (ext.PersonaM
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Description: s.Description,
|
||||
Override: s.Override,
|
||||
})
|
||||
}
|
||||
for _, g := range guardrails {
|
||||
@@ -567,11 +569,12 @@ func (db *DB) RemovePersonaPart(ctx context.Context, personaName, partName strin
|
||||
// Persona-Skill links
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
func (db *DB) AddPersonaSkill(ctx context.Context, personaID, skillID int64) error {
|
||||
func (db *DB) AddPersonaSkill(ctx context.Context, personaID, skillID int64, override bool) error {
|
||||
_, err := db.pool.Exec(ctx, `
|
||||
insert into agent_persona_skills (persona_id, skill_id)
|
||||
values ($1, $2) on conflict do nothing
|
||||
`, personaID, skillID)
|
||||
insert into agent_persona_skills (persona_id, skill_id, override)
|
||||
values ($1, $2, $3)
|
||||
on conflict (persona_id, skill_id) do update set override = excluded.override
|
||||
`, personaID, skillID, override)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add persona skill: %w", err)
|
||||
}
|
||||
@@ -1058,7 +1061,7 @@ func (db *DB) fetchPartsByNames(ctx context.Context, names []string) ([]rawPart,
|
||||
|
||||
func (db *DB) listPersonaSkills(ctx context.Context, personaID int64) ([]AgentSkillRow, error) {
|
||||
rows, err := db.pool.Query(ctx, `
|
||||
select s.id, s.name, s.description, s.content, s.tags::text[]
|
||||
select s.id, s.name, s.description, s.content, s.tags::text[], aps.override
|
||||
from agent_skills s
|
||||
join agent_persona_skills aps on aps.skill_id = s.id
|
||||
where aps.persona_id = $1
|
||||
@@ -1073,7 +1076,7 @@ func (db *DB) listPersonaSkills(ctx context.Context, personaID int64) ([]AgentSk
|
||||
for rows.Next() {
|
||||
var s AgentSkillRow
|
||||
var tags []string
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.Content, &tags); err != nil {
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.Content, &tags, &s.Override); err != nil {
|
||||
return nil, fmt.Errorf("scan persona skill: %w", err)
|
||||
}
|
||||
s.Tags = nilToEmptyStrings(tags)
|
||||
@@ -1139,6 +1142,7 @@ type AgentSkillRow struct {
|
||||
Description string
|
||||
Content string
|
||||
Tags []string
|
||||
Override bool
|
||||
}
|
||||
|
||||
type AgentGuardrailRow struct {
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
|
||||
func (db *DB) InsertStoredFile(ctx context.Context, file thoughttypes.StoredFile) (thoughttypes.StoredFile, error) {
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into stored_files (thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
insert into stored_files (thought_id, project_id, tenant_id, name, media_type, kind, encoding, size_bytes, sha256, content)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
returning id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at
|
||||
`, file.ThoughtID, file.ProjectID, file.Name, file.MediaType, file.Kind, file.Encoding, file.SizeBytes, file.SHA256, file.Content)
|
||||
`, file.ThoughtID, file.ProjectID, tenantKeyPtr(ctx), file.Name, file.MediaType, file.Kind, file.Encoding, file.SizeBytes, file.SHA256, file.Content)
|
||||
|
||||
var model generatedmodels.ModelPublicStoredFiles
|
||||
if err := row.Scan(
|
||||
@@ -42,11 +42,11 @@ func (db *DB) InsertStoredFile(ctx context.Context, file thoughttypes.StoredFile
|
||||
}
|
||||
|
||||
func (db *DB) GetStoredFile(ctx context.Context, id uuid.UUID) (thoughttypes.StoredFile, error) {
|
||||
args := []any{id}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content, created_at, updated_at
|
||||
from stored_files
|
||||
where guid = $1
|
||||
`, id)
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicStoredFiles
|
||||
if err := row.Scan(
|
||||
@@ -77,6 +77,7 @@ func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFil
|
||||
args := make([]any, 0, 4)
|
||||
conditions := make([]string, 0, 3)
|
||||
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
if filter.ThoughtID != nil {
|
||||
args = append(args, *filter.ThoughtID)
|
||||
conditions = append(conditions, fmt.Sprintf("thought_id = $%d", len(args)))
|
||||
|
||||
@@ -475,4 +475,3 @@ func canonicalPlanPair(a, b int64) (int64, int64) {
|
||||
}
|
||||
return b, a
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
func (db *DB) AddProjectPersona(ctx context.Context, projectID, personaID int64, isDefault bool) error {
|
||||
tx, err := db.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin add project persona: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
if _, err := tx.Exec(ctx, `select pg_advisory_xact_lock($1)`, projectID); err != nil {
|
||||
return fmt.Errorf("lock project persona defaults: %w", err)
|
||||
}
|
||||
if isDefault {
|
||||
if _, err := tx.Exec(ctx, `update project_personas set is_default = false where project_id = $1`, projectID); err != nil {
|
||||
return fmt.Errorf("clear default project persona: %w", err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
insert into project_personas (project_id, persona_id, is_default)
|
||||
values ($1, $2, $3)
|
||||
on conflict (project_id, persona_id) do update set is_default = excluded.is_default
|
||||
`, projectID, personaID, isDefault); err != nil {
|
||||
return fmt.Errorf("add project persona: %w", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit add project persona: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) RemoveProjectPersona(ctx context.Context, projectID, personaID int64) error {
|
||||
tag, err := db.pool.Exec(ctx, `delete from project_personas where project_id = $1 and persona_id = $2`, projectID, personaID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove project persona: %w", err)
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return fmt.Errorf("project persona link not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) ListProjectPersonas(ctx context.Context, projectID int64) ([]ext.ProjectPersona, error) {
|
||||
rows, err := db.pool.Query(ctx, `
|
||||
select p.id, p.guid, p.name, p.description, p.summary, p.detail,
|
||||
p.compiled_summary, p.compiled_detail, p.compiled_at, p.tags::text[],
|
||||
p.created_at, p.updated_at, pp.is_default
|
||||
from agent_personas p
|
||||
join project_personas pp on pp.persona_id = p.id
|
||||
where pp.project_id = $1
|
||||
order by pp.is_default desc, p.name
|
||||
`, projectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list project personas: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []ext.ProjectPersona
|
||||
for rows.Next() {
|
||||
var item ext.ProjectPersona
|
||||
var tags []string
|
||||
if err := rows.Scan(&item.Persona.ID, &item.Persona.GUID, &item.Persona.Name,
|
||||
&item.Persona.Description, &item.Persona.Summary, &item.Persona.Detail,
|
||||
&item.Persona.CompiledSummary, &item.Persona.CompiledDetail, &item.Persona.CompiledAt,
|
||||
&tags, &item.Persona.CreatedAt, &item.Persona.UpdatedAt, &item.IsDefault); err != nil {
|
||||
return nil, fmt.Errorf("scan project persona: %w", err)
|
||||
}
|
||||
item.Persona.Tags = nilToEmptyStrings(tags)
|
||||
result = append(result, item)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (db *DB) GetDefaultProjectPersona(ctx context.Context, projectID int64) (*ext.Persona, error) {
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select p.id, p.guid, p.name, p.description, p.summary, p.detail,
|
||||
p.compiled_summary, p.compiled_detail, p.compiled_at, p.tags::text[],
|
||||
p.created_at, p.updated_at
|
||||
from agent_personas p
|
||||
join project_personas pp on pp.persona_id = p.id
|
||||
where pp.project_id = $1 and pp.is_default
|
||||
`, projectID)
|
||||
persona, err := scanPersona(row)
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get default project persona: %w", err)
|
||||
}
|
||||
return &persona, nil
|
||||
}
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
|
||||
func (db *DB) CreateProject(ctx context.Context, name, description string) (thoughttypes.Project, error) {
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into projects (name, description)
|
||||
values ($1, $2)
|
||||
insert into projects (name, description, tenant_id)
|
||||
values ($1, $2, $3)
|
||||
returning id, guid, name, description, created_at, last_active_at
|
||||
`, name, description)
|
||||
`, name, description, tenantKeyPtr(ctx))
|
||||
|
||||
var model generatedmodels.ModelPublicProjects
|
||||
if err := row.Scan(&model.ID, &model.GUID, &model.Name, &model.Description, &model.CreatedAt, &model.LastActiveAt); err != nil {
|
||||
@@ -45,20 +45,20 @@ func (db *DB) GetProject(ctx context.Context, nameOrID string) (thoughttypes.Pro
|
||||
}
|
||||
|
||||
func (db *DB) getProjectByGUID(ctx context.Context, id uuid.UUID) (thoughttypes.Project, error) {
|
||||
args := []any{id}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, name, description, created_at, last_active_at
|
||||
from projects
|
||||
where guid = $1
|
||||
`, id)
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
return scanProject(row)
|
||||
}
|
||||
|
||||
func (db *DB) getProjectByName(ctx context.Context, name string) (thoughttypes.Project, error) {
|
||||
args := []any{name}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, name, description, created_at, last_active_at
|
||||
from projects
|
||||
where name = $1
|
||||
`, name)
|
||||
where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
return scanProject(row)
|
||||
}
|
||||
|
||||
@@ -74,13 +74,20 @@ func scanProject(row pgx.Row) (thoughttypes.Project, error) {
|
||||
}
|
||||
|
||||
func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary, error) {
|
||||
args := []any{}
|
||||
where := ""
|
||||
if key, ok := tenantKey(ctx); ok {
|
||||
args = append(args, key)
|
||||
where = "where p.tenant_id = $1"
|
||||
}
|
||||
rows, err := db.pool.Query(ctx, `
|
||||
select p.id, p.guid, p.name, p.description, p.created_at, p.last_active_at, count(t.id) as thought_count
|
||||
from projects p
|
||||
left join thoughts t on t.project_id = p.id and t.archived_at is null
|
||||
`+where+`
|
||||
group by p.id, p.guid, p.name, p.description, p.created_at, p.last_active_at
|
||||
order by p.last_active_at desc, p.created_at desc
|
||||
`)
|
||||
`, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list projects: %w", err)
|
||||
}
|
||||
@@ -105,7 +112,8 @@ func (db *DB) ListProjects(ctx context.Context) ([]thoughttypes.ProjectSummary,
|
||||
}
|
||||
|
||||
func (db *DB) TouchProject(ctx context.Context, id int64) error {
|
||||
tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where id = $1`, id)
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `update projects set last_active_at = now() where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("touch project: %w", err)
|
||||
}
|
||||
|
||||
+56
-23
@@ -28,10 +28,10 @@ func (db *DB) AddSkill(ctx context.Context, skill ext.AgentSkill) (ext.AgentSkil
|
||||
skill.DomainTags = []string{}
|
||||
}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into agent_skills (name, description, content, tags, language_tags, library_tags, framework_tags, domain_tags)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
insert into agent_skills (name, description, content, tenant_id, tags, language_tags, library_tags, framework_tags, domain_tags)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
returning id, guid, created_at, updated_at
|
||||
`, skill.Name, skill.Description, skill.Content, skill.Tags,
|
||||
`, skill.Name, skill.Description, skill.Content, tenantKeyPtr(ctx), skill.Tags,
|
||||
skill.LanguageTags, skill.LibraryTags, skill.FrameworkTags, skill.DomainTags)
|
||||
|
||||
created := skill
|
||||
@@ -47,7 +47,8 @@ func (db *DB) AddSkill(ctx context.Context, skill ext.AgentSkill) (ext.AgentSkil
|
||||
}
|
||||
|
||||
func (db *DB) RemoveSkill(ctx context.Context, id int64) error {
|
||||
tag, err := db.pool.Exec(ctx, `delete from agent_skills where id = $1`, id)
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `delete from agent_skills where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete agent skill: %w", err)
|
||||
}
|
||||
@@ -60,9 +61,14 @@ func (db *DB) RemoveSkill(ctx context.Context, id int64) error {
|
||||
func (db *DB) ListSkills(ctx context.Context, tag string) ([]ext.AgentSkill, error) {
|
||||
q := `select id, name, description, content, tags::text[], language_tags::text[], library_tags::text[], framework_tags::text[], domain_tags::text[], created_at, updated_at from agent_skills`
|
||||
args := []any{}
|
||||
conditions := []string{}
|
||||
if t := strings.TrimSpace(tag); t != "" {
|
||||
args = append(args, t)
|
||||
q += fmt.Sprintf(" where $%d = any(tags) or $%d = any(language_tags) or $%d = any(library_tags) or $%d = any(framework_tags) or $%d = any(domain_tags)", len(args), len(args), len(args), len(args), len(args))
|
||||
conditions = append(conditions, fmt.Sprintf("($%d = any(tags) or $%d = any(language_tags) or $%d = any(library_tags) or $%d = any(framework_tags) or $%d = any(domain_tags))", len(args), len(args), len(args), len(args), len(args)))
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
if len(conditions) > 0 {
|
||||
q += " where " + strings.Join(conditions, " and ")
|
||||
}
|
||||
q += " order by name"
|
||||
|
||||
@@ -116,8 +122,27 @@ func scanSkill(row skillScanner) (ext.AgentSkill, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeSkillSlices(skill *ext.AgentSkill) {
|
||||
if skill.Tags == nil {
|
||||
skill.Tags = []string{}
|
||||
}
|
||||
if skill.LanguageTags == nil {
|
||||
skill.LanguageTags = []string{}
|
||||
}
|
||||
if skill.LibraryTags == nil {
|
||||
skill.LibraryTags = []string{}
|
||||
}
|
||||
if skill.FrameworkTags == nil {
|
||||
skill.FrameworkTags = []string{}
|
||||
}
|
||||
if skill.DomainTags == nil {
|
||||
skill.DomainTags = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) GetSkill(ctx context.Context, id int64) (ext.AgentSkill, error) {
|
||||
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`, id)
|
||||
args := []any{id}
|
||||
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
s, err := scanSkill(row)
|
||||
if err != nil {
|
||||
return ext.AgentSkill{}, fmt.Errorf("get agent skill: %w", err)
|
||||
@@ -126,7 +151,8 @@ func (db *DB) GetSkill(ctx context.Context, id int64) (ext.AgentSkill, error) {
|
||||
}
|
||||
|
||||
func (db *DB) GetSkillByName(ctx context.Context, name string) (ext.AgentSkill, error) {
|
||||
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where name = $1`, name)
|
||||
args := []any{name}
|
||||
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
s, err := scanSkill(row)
|
||||
if err != nil {
|
||||
return ext.AgentSkill{}, fmt.Errorf("get agent skill by name: %w", err)
|
||||
@@ -135,10 +161,10 @@ func (db *DB) GetSkillByName(ctx context.Context, name string) (ext.AgentSkill,
|
||||
}
|
||||
|
||||
func (db *DB) GetGuardrailByName(ctx context.Context, name string) (ext.AgentGuardrail, error) {
|
||||
args := []any{name}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, name, description, content, severity, tags::text[], created_at, updated_at
|
||||
from agent_guardrails where name = $1
|
||||
`, name)
|
||||
from agent_guardrails where name = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicAgentGuardrails
|
||||
var tags []string
|
||||
@@ -171,10 +197,10 @@ func (db *DB) AddGuardrail(ctx context.Context, g ext.AgentGuardrail) (ext.Agent
|
||||
g.Severity = "medium"
|
||||
}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
insert into agent_guardrails (name, description, content, severity, tags)
|
||||
values ($1, $2, $3, $4, $5)
|
||||
insert into agent_guardrails (name, description, content, severity, tenant_id, tags)
|
||||
values ($1, $2, $3, $4, $5, $6)
|
||||
returning id, guid, created_at, updated_at
|
||||
`, g.Name, g.Description, g.Content, g.Severity, g.Tags)
|
||||
`, g.Name, g.Description, g.Content, g.Severity, tenantKeyPtr(ctx), g.Tags)
|
||||
|
||||
created := g
|
||||
var model generatedmodels.ModelPublicAgentGuardrails
|
||||
@@ -189,7 +215,8 @@ func (db *DB) AddGuardrail(ctx context.Context, g ext.AgentGuardrail) (ext.Agent
|
||||
}
|
||||
|
||||
func (db *DB) RemoveGuardrail(ctx context.Context, id int64) error {
|
||||
tag, err := db.pool.Exec(ctx, `delete from agent_guardrails where id = $1`, id)
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `delete from agent_guardrails where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete agent guardrail: %w", err)
|
||||
}
|
||||
@@ -211,6 +238,7 @@ func (db *DB) ListGuardrails(ctx context.Context, tag, severity string) ([]ext.A
|
||||
args = append(args, s)
|
||||
conditions = append(conditions, fmt.Sprintf("severity = $%d", len(args)))
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
|
||||
q := `select id, name, description, content, severity, tags::text[], created_at, updated_at from agent_guardrails`
|
||||
if len(conditions) > 0 {
|
||||
@@ -250,10 +278,10 @@ func (db *DB) ListGuardrails(ctx context.Context, tag, severity string) ([]ext.A
|
||||
}
|
||||
|
||||
func (db *DB) GetGuardrail(ctx context.Context, id int64) (ext.AgentGuardrail, error) {
|
||||
args := []any{id}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, name, description, content, severity, tags::text[], created_at, updated_at
|
||||
from agent_guardrails where id = $1
|
||||
`, id)
|
||||
from agent_guardrails where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicAgentGuardrails
|
||||
var tags []string
|
||||
@@ -278,12 +306,12 @@ func (db *DB) GetGuardrail(ctx context.Context, id int64) (ext.AgentGuardrail, e
|
||||
|
||||
// Project Skills
|
||||
|
||||
func (db *DB) AddProjectSkill(ctx context.Context, projectID, skillID int64) error {
|
||||
func (db *DB) AddProjectSkill(ctx context.Context, projectID, skillID int64, override bool) error {
|
||||
_, err := db.pool.Exec(ctx, `
|
||||
insert into project_skills (project_id, skill_id)
|
||||
values ($1, $2)
|
||||
on conflict do nothing
|
||||
`, projectID, skillID)
|
||||
insert into project_skills (project_id, skill_id, override)
|
||||
values ($1, $2, $3)
|
||||
on conflict (project_id, skill_id) do update set override = excluded.override
|
||||
`, projectID, skillID, override)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add project skill: %w", err)
|
||||
}
|
||||
@@ -305,7 +333,9 @@ func (db *DB) RemoveProjectSkill(ctx context.Context, projectID, skillID int64)
|
||||
|
||||
func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.AgentSkill, error) {
|
||||
rows, err := db.pool.Query(ctx, `
|
||||
select s.`+skillSelectCols+`
|
||||
select s.id, s.name, s.description, s.content, s.tags::text[],
|
||||
s.language_tags::text[], s.library_tags::text[], s.framework_tags::text[],
|
||||
s.domain_tags::text[], s.created_at, s.updated_at, ps.override
|
||||
from agent_skills s
|
||||
join project_skills ps on ps.skill_id = s.id
|
||||
where ps.project_id = $1
|
||||
@@ -318,10 +348,13 @@ func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.Age
|
||||
|
||||
var skills []ext.AgentSkill
|
||||
for rows.Next() {
|
||||
s, err := scanSkill(rows)
|
||||
if err != nil {
|
||||
var s ext.AgentSkill
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.Content, &s.Tags,
|
||||
&s.LanguageTags, &s.LibraryTags, &s.FrameworkTags, &s.DomainTags,
|
||||
&s.CreatedAt, &s.UpdatedAt, &s.Override); err != nil {
|
||||
return nil, fmt.Errorf("scan project skill: %w", err)
|
||||
}
|
||||
normalizeSkillSlices(&s)
|
||||
skills = append(skills, s)
|
||||
}
|
||||
return skills, rows.Err()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/tenancy"
|
||||
)
|
||||
|
||||
func tenantKeyPtr(ctx context.Context) *string {
|
||||
if key, ok := tenancy.KeyFromContext(ctx); ok {
|
||||
return &key
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tenantKey(ctx context.Context) (string, bool) {
|
||||
return tenancy.KeyFromContext(ctx)
|
||||
}
|
||||
|
||||
func addTenantCondition(ctx context.Context, args *[]any, conditions *[]string, column string) {
|
||||
if key, ok := tenancy.KeyFromContext(ctx); ok {
|
||||
*args = append(*args, key)
|
||||
*conditions = append(*conditions, fmt.Sprintf("%s = $%d", column, len(*args)))
|
||||
}
|
||||
}
|
||||
|
||||
func tenantSQL(ctx context.Context, args *[]any, column string) string {
|
||||
if key, ok := tenancy.KeyFromContext(ctx); ok {
|
||||
*args = append(*args, key)
|
||||
return fmt.Sprintf(" and %s = $%d", column, len(*args))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
// LinkThoughtLearning creates or updates an association between a thought (by GUID) and a learning (by numeric ID).
|
||||
// If the pair already exists, the relation label is updated.
|
||||
func (db *DB) LinkThoughtLearning(ctx context.Context, thoughtGUID uuid.UUID, learningID int64, relation string) error {
|
||||
_, err := db.pool.Exec(ctx, `
|
||||
insert into thought_learning_links (thought_id, learning_id, relation)
|
||||
select t.id, $2, $3
|
||||
from thoughts t
|
||||
where t.guid = $1
|
||||
on conflict (thought_id, learning_id) do update set relation = excluded.relation
|
||||
`, thoughtGUID, learningID, relation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("link thought learning: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnlinkThoughtLearning removes the association between a thought (by GUID) and a learning (by numeric ID).
|
||||
func (db *DB) UnlinkThoughtLearning(ctx context.Context, thoughtGUID uuid.UUID, learningID int64) error {
|
||||
_, err := db.pool.Exec(ctx, `
|
||||
delete from thought_learning_links
|
||||
where thought_id = (select id from thoughts where guid = $1)
|
||||
and learning_id = $2
|
||||
`, thoughtGUID, learningID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unlink thought learning: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLinkedLearnings returns all learnings explicitly associated with a thought (by GUID).
|
||||
func (db *DB) GetLinkedLearnings(ctx context.Context, thoughtGUID uuid.UUID) ([]thoughttypes.LinkedLearning, error) {
|
||||
rows, err := db.pool.Query(ctx, `
|
||||
select l.id, l.guid, l.summary, l.details, l.category, l.area, l.status, l.priority, l.confidence,
|
||||
l.action_required, l.source_type, l.source_ref, l.project_id, l.related_thought_id,
|
||||
l.related_skill_id, l.reviewed_by, l.reviewed_at, l.duplicate_of_learning_id,
|
||||
l.supersedes_learning_id, l.tags::text[], l.created_at, l.updated_at,
|
||||
tll.relation, tll.created_at as linked_at
|
||||
from thought_learning_links tll
|
||||
join learnings l on l.id = tll.learning_id
|
||||
where tll.thought_id = (select id from thoughts where guid = $1)
|
||||
order by tll.created_at desc
|
||||
`, thoughtGUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query linked learnings: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]thoughttypes.LinkedLearning, 0)
|
||||
for rows.Next() {
|
||||
var tags []string
|
||||
var linked thoughttypes.LinkedLearning
|
||||
var m generatedmodels.ModelPublicLearnings
|
||||
|
||||
if err := rows.Scan(
|
||||
&m.ID,
|
||||
&m.GUID,
|
||||
&m.Summary,
|
||||
&m.Details,
|
||||
&m.Category,
|
||||
&m.Area,
|
||||
&m.Status,
|
||||
&m.Priority,
|
||||
&m.Confidence,
|
||||
&m.ActionRequired,
|
||||
&m.SourceType,
|
||||
&m.SourceRef,
|
||||
&m.ProjectID,
|
||||
&m.RelatedThoughtID,
|
||||
&m.RelatedSkillID,
|
||||
&m.ReviewedBy,
|
||||
&m.ReviewedAt,
|
||||
&m.DuplicateOfLearningID,
|
||||
&m.SupersedesLearningID,
|
||||
&tags,
|
||||
&m.CreatedAt,
|
||||
&m.UpdatedAt,
|
||||
&linked.Relation,
|
||||
&linked.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan linked learning: %w", err)
|
||||
}
|
||||
linked.Learning = learningFromModel(m, tags)
|
||||
items = append(items, linked)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate linked learnings: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetLinkedThoughtsForLearning returns all thoughts explicitly associated with a learning (by numeric ID).
|
||||
func (db *DB) GetLinkedThoughtsForLearning(ctx context.Context, learningID int64) ([]thoughttypes.LinkedLearningThought, error) {
|
||||
rows, err := db.pool.Query(ctx, `
|
||||
select t.id, t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at,
|
||||
tll.relation, tll.created_at as linked_at
|
||||
from thought_learning_links tll
|
||||
join thoughts t on t.id = tll.thought_id
|
||||
where tll.learning_id = $1
|
||||
order by tll.created_at desc
|
||||
`, learningID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query linked thoughts for learning: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]thoughttypes.LinkedLearningThought, 0)
|
||||
for rows.Next() {
|
||||
var linked thoughttypes.LinkedLearningThought
|
||||
var m generatedmodels.ModelPublicThoughts
|
||||
|
||||
if err := rows.Scan(
|
||||
&m.ID,
|
||||
&m.GUID,
|
||||
&m.Content,
|
||||
&m.Metadata,
|
||||
&m.ProjectID,
|
||||
&m.ArchivedAt,
|
||||
&m.CreatedAt,
|
||||
&m.UpdatedAt,
|
||||
&linked.Relation,
|
||||
&linked.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan linked thought for learning: %w", err)
|
||||
}
|
||||
thought, err := thoughtFromModel(m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("map linked thought for learning: %w", err)
|
||||
}
|
||||
linked.Thought = thought
|
||||
items = append(items, linked)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate linked thoughts for learning: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
+42
-15
@@ -31,10 +31,10 @@ func (db *DB) InsertThought(ctx context.Context, thought thoughttypes.Thought, e
|
||||
}()
|
||||
|
||||
row := tx.QueryRow(ctx, `
|
||||
insert into thoughts (content, metadata, project_id)
|
||||
values ($1, $2::jsonb, $3)
|
||||
insert into thoughts (content, metadata, project_id, tenant_id)
|
||||
values ($1, $2::jsonb, $3, $4)
|
||||
returning id, guid, created_at, updated_at
|
||||
`, thought.Content, metadata, thought.ProjectID)
|
||||
`, thought.Content, metadata, thought.ProjectID, tenantKeyPtr(ctx))
|
||||
|
||||
created := thought
|
||||
created.Embedding = nil
|
||||
@@ -68,6 +68,22 @@ func (db *DB) InsertThought(ctx context.Context, thought thoughttypes.Thought, e
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (db *DB) GetThoughtByWebhookIDempotencyKey(ctx context.Context, key string) (thoughttypes.Thought, error) {
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
||||
from thoughts
|
||||
where metadata->'webhook'->>'idempotency_key' = $1
|
||||
order by created_at desc
|
||||
limit 1
|
||||
`, strings.TrimSpace(key))
|
||||
|
||||
var model generatedmodels.ModelPublicThoughts
|
||||
if err := row.Scan(&model.ID, &model.GUID, &model.Content, &model.Metadata, &model.ProjectID, &model.ArchivedAt, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
return thoughttypes.Thought{}, err
|
||||
}
|
||||
return thoughtFromModel(model)
|
||||
}
|
||||
|
||||
func (db *DB) SearchThoughts(ctx context.Context, embedding []float32, embeddingModel string, threshold float64, limit int, filter map[string]any) ([]thoughttypes.SearchResult, error) {
|
||||
filterJSON, err := json.Marshal(filter)
|
||||
if err != nil {
|
||||
@@ -107,6 +123,7 @@ func (db *DB) ListThoughts(ctx context.Context, filter thoughttypes.ListFilter)
|
||||
args := make([]any, 0, 6)
|
||||
conditions := []string{}
|
||||
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
if !filter.IncludeArchived {
|
||||
conditions = append(conditions, "archived_at is null")
|
||||
}
|
||||
@@ -170,11 +187,14 @@ func (db *DB) ListThoughts(ctx context.Context, filter thoughttypes.ListFilter)
|
||||
|
||||
func (db *DB) Stats(ctx context.Context) (thoughttypes.ThoughtStats, error) {
|
||||
var total int
|
||||
if err := db.pool.QueryRow(ctx, `select count(*) from thoughts where archived_at is null`).Scan(&total); err != nil {
|
||||
statsArgs := []any{}
|
||||
statsConditions := []string{"archived_at is null"}
|
||||
addTenantCondition(ctx, &statsArgs, &statsConditions, "tenant_id")
|
||||
if err := db.pool.QueryRow(ctx, `select count(*) from thoughts where `+strings.Join(statsConditions, " and "), statsArgs...).Scan(&total); err != nil {
|
||||
return thoughttypes.ThoughtStats{}, fmt.Errorf("count thoughts: %w", err)
|
||||
}
|
||||
|
||||
rows, err := db.pool.Query(ctx, `select metadata from thoughts where archived_at is null`)
|
||||
rows, err := db.pool.Query(ctx, `select metadata from thoughts where `+strings.Join(statsConditions, " and "), statsArgs...)
|
||||
if err != nil {
|
||||
return thoughttypes.ThoughtStats{}, fmt.Errorf("query stats metadata: %w", err)
|
||||
}
|
||||
@@ -217,11 +237,11 @@ func (db *DB) Stats(ctx context.Context) (thoughttypes.ThoughtStats, error) {
|
||||
}
|
||||
|
||||
func (db *DB) GetThought(ctx context.Context, id uuid.UUID) (thoughttypes.Thought, error) {
|
||||
args := []any{id}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
||||
from thoughts
|
||||
where guid = $1
|
||||
`, id)
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicThoughts
|
||||
if err := row.Scan(&model.ID, &model.GUID, &model.Content, &model.Metadata, &model.ProjectID, &model.ArchivedAt, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
@@ -240,11 +260,11 @@ func (db *DB) GetThought(ctx context.Context, id uuid.UUID) (thoughttypes.Though
|
||||
}
|
||||
|
||||
func (db *DB) GetThoughtByID(ctx context.Context, id int64) (thoughttypes.Thought, error) {
|
||||
args := []any{id}
|
||||
row := db.pool.QueryRow(ctx, `
|
||||
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
||||
from thoughts
|
||||
where id = $1
|
||||
`, id)
|
||||
where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
|
||||
var model generatedmodels.ModelPublicThoughts
|
||||
if err := row.Scan(&model.ID, &model.GUID, &model.Content, &model.Metadata, &model.ProjectID, &model.ArchivedAt, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
@@ -276,14 +296,14 @@ func (db *DB) UpdateThought(ctx context.Context, id uuid.UUID, content string, e
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
args := []any{id, content, metadataBytes, projectID}
|
||||
tag, err := tx.Exec(ctx, `
|
||||
update thoughts
|
||||
set content = $2,
|
||||
metadata = $3::jsonb,
|
||||
project_id = $4,
|
||||
updated_at = now()
|
||||
where guid = $1
|
||||
`, id, content, metadataBytes, projectID)
|
||||
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return thoughttypes.Thought{}, fmt.Errorf("update thought: %w", err)
|
||||
}
|
||||
@@ -317,12 +337,12 @@ func (db *DB) UpdateThoughtMetadata(ctx context.Context, id int64, metadata thou
|
||||
return thoughttypes.Thought{}, fmt.Errorf("marshal updated metadata: %w", err)
|
||||
}
|
||||
|
||||
args := []any{id, metadataBytes}
|
||||
tag, err := db.pool.Exec(ctx, `
|
||||
update thoughts
|
||||
set metadata = $2::jsonb,
|
||||
updated_at = now()
|
||||
where id = $1
|
||||
`, id, metadataBytes)
|
||||
where id = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return thoughttypes.Thought{}, fmt.Errorf("update thought metadata: %w", err)
|
||||
}
|
||||
@@ -334,7 +354,8 @@ func (db *DB) UpdateThoughtMetadata(ctx context.Context, id int64, metadata thou
|
||||
}
|
||||
|
||||
func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error {
|
||||
tag, err := db.pool.Exec(ctx, `delete from thoughts where guid = $1`, id)
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `delete from thoughts where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete thought: %w", err)
|
||||
}
|
||||
@@ -345,7 +366,8 @@ func (db *DB) DeleteThought(ctx context.Context, id uuid.UUID) error {
|
||||
}
|
||||
|
||||
func (db *DB) ArchiveThought(ctx context.Context, id uuid.UUID) error {
|
||||
tag, err := db.pool.Exec(ctx, `update thoughts set archived_at = now(), updated_at = now() where guid = $1`, id)
|
||||
args := []any{id}
|
||||
tag, err := db.pool.Exec(ctx, `update thoughts set archived_at = now(), updated_at = now() where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("archive thought: %w", err)
|
||||
}
|
||||
@@ -424,6 +446,7 @@ func (db *DB) SearchSimilarThoughts(ctx context.Context, embedding []float32, em
|
||||
"1 - (e.embedding <=> $1) > $2",
|
||||
"e.model = $3",
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_id")
|
||||
if projectID != nil {
|
||||
args = append(args, *projectID)
|
||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
||||
@@ -472,6 +495,7 @@ func (db *DB) HasEmbeddingsForModel(ctx context.Context, model string, projectID
|
||||
"e.model = $1",
|
||||
"t.archived_at is null",
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_id")
|
||||
if projectID != nil {
|
||||
args = append(args, *projectID)
|
||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
||||
@@ -490,6 +514,7 @@ func (db *DB) HasEmbeddingsForModel(ctx context.Context, model string, projectID
|
||||
func (db *DB) ListThoughtsMissingEmbedding(ctx context.Context, model string, limit int, projectID *int64, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) {
|
||||
args := []any{model}
|
||||
conditions := []string{"e.id is null"}
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_id")
|
||||
|
||||
if !includeArchived {
|
||||
conditions = append(conditions, "t.archived_at is null")
|
||||
@@ -539,6 +564,7 @@ func (db *DB) ListThoughtsMissingEmbedding(ctx context.Context, model string, li
|
||||
func (db *DB) ListThoughtsForMetadataReparse(ctx context.Context, limit int, projectID *int64, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) {
|
||||
args := make([]any, 0, 3)
|
||||
conditions := make([]string, 0, 4)
|
||||
addTenantCondition(ctx, &args, &conditions, "tenant_id")
|
||||
|
||||
if !includeArchived {
|
||||
conditions = append(conditions, "archived_at is null")
|
||||
@@ -608,6 +634,7 @@ func (db *DB) SearchThoughtsText(ctx context.Context, query string, limit int, p
|
||||
"t.archived_at is null",
|
||||
"(to_tsvector('simple', t.content) || to_tsvector('simple', coalesce(p.name, ''))) @@ websearch_to_tsquery('simple', $1)",
|
||||
}
|
||||
addTenantCondition(ctx, &args, &conditions, "t.tenant_id")
|
||||
if projectID != nil {
|
||||
args = append(args, *projectID)
|
||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package tenancy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const tenantKeyContextKey contextKey = "tenancy.tenant_key"
|
||||
|
||||
// WithTenantKey returns a context scoped to the authenticated tenant boundary.
|
||||
// The tenant key is intentionally opaque; today it is the authenticated API key
|
||||
// or OAuth client id, and callers should not interpret it as a human username.
|
||||
func WithTenantKey(ctx context.Context, tenantKey string) context.Context {
|
||||
tenantKey = strings.TrimSpace(tenantKey)
|
||||
if tenantKey == "" {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, tenantKeyContextKey, tenantKey)
|
||||
}
|
||||
|
||||
func KeyFromContext(ctx context.Context) (string, bool) {
|
||||
if ctx == nil {
|
||||
return "", false
|
||||
}
|
||||
value, ok := ctx.Value(tenantKeyContextKey).(string)
|
||||
value = strings.TrimSpace(value)
|
||||
return value, ok && value != ""
|
||||
}
|
||||
@@ -422,6 +422,7 @@ func (t *AgentPersonasTool) RemovePersonaPart(ctx context.Context, _ *mcp.CallTo
|
||||
type AddPersonaSkillInput struct {
|
||||
PersonaID int64 `json:"persona_id" jsonschema:"persona id"`
|
||||
SkillID int64 `json:"skill_id" jsonschema:"agent skill id to link"`
|
||||
Override bool `json:"override,omitempty" jsonschema:"replace a project skill with the same name during world-model assembly"`
|
||||
}
|
||||
|
||||
type AddPersonaSkillOutput struct {
|
||||
@@ -430,7 +431,7 @@ type AddPersonaSkillOutput struct {
|
||||
}
|
||||
|
||||
func (t *AgentPersonasTool) AddPersonaSkill(ctx context.Context, _ *mcp.CallToolRequest, in AddPersonaSkillInput) (*mcp.CallToolResult, AddPersonaSkillOutput, error) {
|
||||
if err := t.store.AddPersonaSkill(ctx, in.PersonaID, in.SkillID); err != nil {
|
||||
if err := t.store.AddPersonaSkill(ctx, in.PersonaID, in.SkillID, in.Override); err != nil {
|
||||
return nil, AddPersonaSkillOutput{}, err
|
||||
}
|
||||
return nil, AddPersonaSkillOutput{PersonaID: in.PersonaID, SkillID: in.SkillID}, nil
|
||||
|
||||
@@ -36,9 +36,10 @@ type ContextItem struct {
|
||||
}
|
||||
|
||||
type ProjectContextOutput struct {
|
||||
Project thoughttypes.Project `json:"project"`
|
||||
Context string `json:"context"`
|
||||
Items []ContextItem `json:"items"`
|
||||
Project thoughttypes.Project `json:"project"`
|
||||
Context string `json:"context"`
|
||||
Items []ContextItem `json:"items"`
|
||||
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||
}
|
||||
|
||||
func NewContextTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *ContextTool {
|
||||
@@ -70,12 +71,14 @@ func (t *ContextTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in P
|
||||
})
|
||||
}
|
||||
|
||||
var retrievalMode string
|
||||
query := strings.TrimSpace(in.Query)
|
||||
if query != "" {
|
||||
semantic, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, &project.NumericID, nil)
|
||||
semantic, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, &project.NumericID, nil)
|
||||
if err != nil {
|
||||
return nil, ProjectContextOutput{}, err
|
||||
}
|
||||
retrievalMode = mode
|
||||
for _, result := range semantic {
|
||||
key := fmt.Sprint(result.ID)
|
||||
if _, ok := seen[key]; ok {
|
||||
@@ -100,8 +103,9 @@ func (t *ContextTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in P
|
||||
_ = t.store.TouchProject(ctx, project.NumericID)
|
||||
|
||||
return nil, ProjectContextOutput{
|
||||
Project: *project,
|
||||
Context: contextBlock,
|
||||
Items: items,
|
||||
Project: *project,
|
||||
Context: contextBlock,
|
||||
Items: items,
|
||||
RetrievalMode: retrievalMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/config"
|
||||
"git.warky.dev/wdevs/amcs/internal/session"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
const defaultDuplicateAuditLimit = 50
|
||||
|
||||
var duplicateWhitespace = regexp.MustCompile(`\s+`)
|
||||
|
||||
type DuplicateAuditTool struct {
|
||||
store *store.DB
|
||||
search config.SearchConfig
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
type DuplicateAuditInput struct {
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id to scope thought duplicate scanning"`
|
||||
Limit int `json:"limit,omitempty" jsonschema:"maximum candidate groups to return"`
|
||||
ThoughtLimit int `json:"thought_limit,omitempty" jsonschema:"maximum thoughts to scan for duplicate content; defaults to search limit"`
|
||||
IncludeArchived bool `json:"include_archived,omitempty" jsonschema:"include archived thoughts in the scan"`
|
||||
}
|
||||
|
||||
type DuplicateAuditOutput struct {
|
||||
Report DuplicateAuditReport `json:"report"`
|
||||
}
|
||||
|
||||
type DuplicateAuditReport struct {
|
||||
Summary DuplicateAuditSummary `json:"summary"`
|
||||
ProjectCandidates []DuplicateProjectCandidate `json:"project_candidates"`
|
||||
ThoughtCandidates []DuplicateThoughtCandidate `json:"thought_candidates"`
|
||||
MetadataCandidates []MetadataCleanupCandidate `json:"metadata_candidates"`
|
||||
RecommendedNextStep string `json:"recommended_next_step"`
|
||||
}
|
||||
|
||||
type DuplicateAuditSummary struct {
|
||||
DryRun bool `json:"dry_run"`
|
||||
ScannedProjects int `json:"scanned_projects"`
|
||||
ScannedThoughts int `json:"scanned_thoughts"`
|
||||
ProjectCandidateGroups int `json:"project_candidate_groups"`
|
||||
ThoughtCandidateGroups int `json:"thought_candidate_groups"`
|
||||
MetadataCandidateGroups int `json:"metadata_candidate_groups"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
type DuplicateProjectCandidate struct {
|
||||
MatchType string `json:"match_type"`
|
||||
NormalizedValue string `json:"normalized_value"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
RecommendedCanonicalID uuid.UUID `json:"recommended_canonical_id"`
|
||||
Projects []thoughttypes.ProjectSummary `json:"projects"`
|
||||
}
|
||||
|
||||
type DuplicateThoughtCandidate struct {
|
||||
MatchType string `json:"match_type"`
|
||||
NormalizedValue string `json:"normalized_value"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
RecommendedCanonicalID uuid.UUID `json:"recommended_canonical_id"`
|
||||
Thoughts []thoughttypes.Thought `json:"thoughts"`
|
||||
}
|
||||
|
||||
type MetadataCleanupCandidate struct {
|
||||
Field string `json:"field"`
|
||||
NormalizedValue string `json:"normalized_value"`
|
||||
Values []string `json:"values"`
|
||||
Occurrences int `json:"occurrences"`
|
||||
RecommendedValue string `json:"recommended_value"`
|
||||
RecommendedAction string `json:"recommended_action"`
|
||||
}
|
||||
|
||||
func NewDuplicateAuditTool(db *store.DB, search config.SearchConfig, sessions *session.ActiveProjects) *DuplicateAuditTool {
|
||||
return &DuplicateAuditTool{store: db, search: search, sessions: sessions}
|
||||
}
|
||||
|
||||
func (t *DuplicateAuditTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in DuplicateAuditInput) (*mcp.CallToolResult, DuplicateAuditOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||
if err != nil {
|
||||
return nil, DuplicateAuditOutput{}, err
|
||||
}
|
||||
var projectID *int64
|
||||
if project != nil {
|
||||
projectID = &project.NumericID
|
||||
}
|
||||
|
||||
projects, err := t.store.ListProjects(ctx)
|
||||
if err != nil {
|
||||
return nil, DuplicateAuditOutput{}, err
|
||||
}
|
||||
|
||||
thoughtLimit := normalizeLimit(in.ThoughtLimit, t.search)
|
||||
if in.ThoughtLimit <= 0 {
|
||||
thoughtLimit = normalizeLimit(0, t.search)
|
||||
}
|
||||
thoughts, err := t.store.ListThoughts(ctx, thoughttypes.ListFilter{Limit: thoughtLimit, ProjectID: projectID, IncludeArchived: in.IncludeArchived})
|
||||
if err != nil {
|
||||
return nil, DuplicateAuditOutput{}, err
|
||||
}
|
||||
|
||||
if project != nil {
|
||||
_ = t.store.TouchProject(ctx, project.NumericID)
|
||||
}
|
||||
return nil, DuplicateAuditOutput{Report: buildDuplicateAuditReport(projects, thoughts, in)}, nil
|
||||
}
|
||||
|
||||
func buildDuplicateAuditReport(projects []thoughttypes.ProjectSummary, thoughts []thoughttypes.Thought, in DuplicateAuditInput) DuplicateAuditReport {
|
||||
limit := in.Limit
|
||||
if limit <= 0 {
|
||||
limit = defaultDuplicateAuditLimit
|
||||
}
|
||||
|
||||
projectCandidates, projectTruncated := duplicateProjectCandidates(projects, limit)
|
||||
thoughtCandidates, thoughtTruncated := duplicateThoughtCandidates(thoughts, limit)
|
||||
metadataCandidates, metadataTruncated := metadataCleanupCandidates(thoughts, limit)
|
||||
|
||||
return DuplicateAuditReport{
|
||||
Summary: DuplicateAuditSummary{
|
||||
DryRun: true,
|
||||
ScannedProjects: len(projects),
|
||||
ScannedThoughts: len(thoughts),
|
||||
ProjectCandidateGroups: len(projectCandidates),
|
||||
ThoughtCandidateGroups: len(thoughtCandidates),
|
||||
MetadataCandidateGroups: len(metadataCandidates),
|
||||
Truncated: projectTruncated || thoughtTruncated || metadataTruncated,
|
||||
},
|
||||
ProjectCandidates: projectCandidates,
|
||||
ThoughtCandidates: thoughtCandidates,
|
||||
MetadataCandidates: metadataCandidates,
|
||||
RecommendedNextStep: "Review candidates manually, then use explicit archive/update/merge tools; this audit performs no writes.",
|
||||
}
|
||||
}
|
||||
|
||||
func duplicateProjectCandidates(projects []thoughttypes.ProjectSummary, limit int) ([]DuplicateProjectCandidate, bool) {
|
||||
groups := map[string][]thoughttypes.ProjectSummary{}
|
||||
for _, project := range projects {
|
||||
key := normalizeDuplicateText(project.Name)
|
||||
if key != "" {
|
||||
groups[key] = append(groups[key], project)
|
||||
}
|
||||
}
|
||||
keys := sortedDuplicateKeys(groups)
|
||||
out := make([]DuplicateProjectCandidate, 0, min(len(keys), limit))
|
||||
for _, key := range keys {
|
||||
items := groups[key]
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].ThoughtCount != items[j].ThoughtCount {
|
||||
return items[i].ThoughtCount > items[j].ThoughtCount
|
||||
}
|
||||
return items[i].CreatedAt.Before(items[j].CreatedAt)
|
||||
})
|
||||
matchType := "normalized_name"
|
||||
if allProjectNamesExact(items) {
|
||||
matchType = "exact_name"
|
||||
}
|
||||
out = append(out, DuplicateProjectCandidate{MatchType: matchType, NormalizedValue: key, Confidence: 1.0, RecommendedCanonicalID: items[0].ID, Projects: items})
|
||||
if len(out) == limit {
|
||||
return out, len(keys) > limit
|
||||
}
|
||||
}
|
||||
return out, false
|
||||
}
|
||||
|
||||
func duplicateThoughtCandidates(thoughts []thoughttypes.Thought, limit int) ([]DuplicateThoughtCandidate, bool) {
|
||||
groups := map[string][]thoughttypes.Thought{}
|
||||
for _, thought := range thoughts {
|
||||
key := normalizeDuplicateText(thought.Content)
|
||||
if key != "" {
|
||||
groups[key] = append(groups[key], thought)
|
||||
}
|
||||
}
|
||||
keys := sortedDuplicateKeys(groups)
|
||||
out := make([]DuplicateThoughtCandidate, 0, min(len(keys), limit))
|
||||
for _, key := range keys {
|
||||
items := groups[key]
|
||||
sort.SliceStable(items, func(i, j int) bool { return items[i].CreatedAt.Before(items[j].CreatedAt) })
|
||||
matchType := "normalized_content"
|
||||
if allThoughtContentExact(items) {
|
||||
matchType = "exact_content"
|
||||
}
|
||||
out = append(out, DuplicateThoughtCandidate{MatchType: matchType, NormalizedValue: key, Confidence: 1.0, RecommendedCanonicalID: items[0].GUID, Thoughts: items})
|
||||
if len(out) == limit {
|
||||
return out, len(keys) > limit
|
||||
}
|
||||
}
|
||||
return out, false
|
||||
}
|
||||
|
||||
func metadataCleanupCandidates(thoughts []thoughttypes.Thought, limit int) ([]MetadataCleanupCandidate, bool) {
|
||||
groups := map[string]map[string]int{}
|
||||
add := func(field, value string) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
key := normalizeDuplicateText(trimmed)
|
||||
if key == "" || trimmed == "" {
|
||||
return
|
||||
}
|
||||
bucketKey := field + "\x00" + key
|
||||
if groups[bucketKey] == nil {
|
||||
groups[bucketKey] = map[string]int{}
|
||||
}
|
||||
groups[bucketKey][trimmed]++
|
||||
}
|
||||
for _, thought := range thoughts {
|
||||
add("type", thought.Metadata.Type)
|
||||
add("source", thought.Metadata.Source)
|
||||
for _, topic := range thought.Metadata.Topics {
|
||||
add("topics", topic)
|
||||
}
|
||||
for _, person := range thought.Metadata.People {
|
||||
add("people", person)
|
||||
}
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(groups))
|
||||
for key, values := range groups {
|
||||
if len(values) > 1 {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
out := make([]MetadataCleanupCandidate, 0, min(len(keys), limit))
|
||||
for _, key := range keys {
|
||||
parts := strings.SplitN(key, "\x00", 2)
|
||||
values, occurrences, recommended := valuesByCount(groups[key])
|
||||
out = append(out, MetadataCleanupCandidate{
|
||||
Field: parts[0],
|
||||
NormalizedValue: parts[1],
|
||||
Values: values,
|
||||
Occurrences: occurrences,
|
||||
RecommendedValue: recommended,
|
||||
RecommendedAction: "preview bulk metadata remap before applying; no changes made by audit",
|
||||
})
|
||||
if len(out) == limit {
|
||||
return out, len(keys) > limit
|
||||
}
|
||||
}
|
||||
return out, false
|
||||
}
|
||||
|
||||
func sortedDuplicateKeys[T any](groups map[string][]T) []string {
|
||||
keys := make([]string, 0, len(groups))
|
||||
for key, items := range groups {
|
||||
if len(items) > 1 {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func valuesByCount(counts map[string]int) ([]string, int, string) {
|
||||
values := make([]string, 0, len(counts))
|
||||
occurrences := 0
|
||||
for value, count := range counts {
|
||||
values = append(values, value)
|
||||
occurrences += count
|
||||
}
|
||||
sort.Slice(values, func(i, j int) bool {
|
||||
if counts[values[i]] != counts[values[j]] {
|
||||
return counts[values[i]] > counts[values[j]]
|
||||
}
|
||||
return values[i] < values[j]
|
||||
})
|
||||
return values, occurrences, values[0]
|
||||
}
|
||||
|
||||
func normalizeDuplicateText(value string) string {
|
||||
return duplicateWhitespace.ReplaceAllString(strings.ToLower(strings.TrimSpace(value)), " ")
|
||||
}
|
||||
|
||||
func allProjectNamesExact(projects []thoughttypes.ProjectSummary) bool {
|
||||
if len(projects) == 0 {
|
||||
return false
|
||||
}
|
||||
first := strings.TrimSpace(projects[0].Name)
|
||||
for _, project := range projects[1:] {
|
||||
if strings.TrimSpace(project.Name) != first {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func allThoughtContentExact(thoughts []thoughttypes.Thought) bool {
|
||||
if len(thoughts) == 0 {
|
||||
return false
|
||||
}
|
||||
first := strings.TrimSpace(thoughts[0].Content)
|
||||
for _, thought := range thoughts[1:] {
|
||||
if strings.TrimSpace(thought.Content) != first {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
func TestBuildDuplicateAuditGroupsProjectsAndThoughtsSafely(t *testing.T) {
|
||||
now := time.Date(2026, 4, 12, 20, 5, 0, 0, time.UTC)
|
||||
projects := []thoughttypes.ProjectSummary{
|
||||
{Project: thoughttypes.Project{ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), NumericID: 1, Name: "AMCS", CreatedAt: now}, ThoughtCount: 3},
|
||||
{Project: thoughttypes.Project{ID: uuid.MustParse("22222222-2222-2222-2222-222222222222"), NumericID: 2, Name: "amcs ", CreatedAt: now.Add(time.Hour)}, ThoughtCount: 1},
|
||||
{Project: thoughttypes.Project{ID: uuid.MustParse("33333333-3333-3333-3333-333333333333"), NumericID: 3, Name: "other", CreatedAt: now}, ThoughtCount: 1},
|
||||
}
|
||||
thoughts := []thoughttypes.Thought{
|
||||
{ID: 10, GUID: uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), Content: "Same text", Metadata: thoughttypes.ThoughtMetadata{Type: "Note", Topics: []string{"Go", "go"}, People: []string{"Alice"}}, CreatedAt: now},
|
||||
{ID: 11, GUID: uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), Content: " same text ", Metadata: thoughttypes.ThoughtMetadata{Type: "note", Topics: []string{"go"}, People: []string{"alice"}}, CreatedAt: now.Add(time.Hour)},
|
||||
{ID: 12, GUID: uuid.MustParse("cccccccc-cccc-cccc-cccc-cccccccccccc"), Content: "unique", Metadata: thoughttypes.ThoughtMetadata{Type: "Task", Topics: []string{"Tasks"}}, CreatedAt: now},
|
||||
}
|
||||
|
||||
report := buildDuplicateAuditReport(projects, thoughts, DuplicateAuditInput{})
|
||||
|
||||
if len(report.ProjectCandidates) != 1 {
|
||||
t.Fatalf("project candidates = %d, want 1: %#v", len(report.ProjectCandidates), report.ProjectCandidates)
|
||||
}
|
||||
projectCandidate := report.ProjectCandidates[0]
|
||||
if projectCandidate.MatchType != "normalized_name" || projectCandidate.NormalizedValue != "amcs" || len(projectCandidate.Projects) != 2 {
|
||||
t.Fatalf("project candidate mismatch: %#v", projectCandidate)
|
||||
}
|
||||
if projectCandidate.RecommendedCanonicalID != projects[0].ID {
|
||||
t.Fatalf("recommended project = %s, want %s", projectCandidate.RecommendedCanonicalID, projects[0].ID)
|
||||
}
|
||||
|
||||
if len(report.ThoughtCandidates) != 1 {
|
||||
t.Fatalf("thought candidates = %d, want 1: %#v", len(report.ThoughtCandidates), report.ThoughtCandidates)
|
||||
}
|
||||
thoughtCandidate := report.ThoughtCandidates[0]
|
||||
if thoughtCandidate.MatchType != "normalized_content" || thoughtCandidate.NormalizedValue != "same text" || len(thoughtCandidate.Thoughts) != 2 {
|
||||
t.Fatalf("thought candidate mismatch: %#v", thoughtCandidate)
|
||||
}
|
||||
if thoughtCandidate.RecommendedCanonicalID != thoughts[0].GUID {
|
||||
t.Fatalf("recommended thought = %s, want %s", thoughtCandidate.RecommendedCanonicalID, thoughts[0].GUID)
|
||||
}
|
||||
|
||||
if len(report.MetadataCandidates) != 3 {
|
||||
t.Fatalf("metadata candidates = %d, want 3: %#v", len(report.MetadataCandidates), report.MetadataCandidates)
|
||||
}
|
||||
if report.Summary.ProjectCandidateGroups != 1 || report.Summary.ThoughtCandidateGroups != 1 || report.Summary.MetadataCandidateGroups != 3 {
|
||||
t.Fatalf("summary mismatch: %#v", report.Summary)
|
||||
}
|
||||
if report.Summary.DryRun != true {
|
||||
t.Fatalf("dry run = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDuplicateAuditRespectsLimit(t *testing.T) {
|
||||
projects := []thoughttypes.ProjectSummary{
|
||||
{Project: thoughttypes.Project{ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), Name: "Alpha"}},
|
||||
{Project: thoughttypes.Project{ID: uuid.MustParse("22222222-2222-2222-2222-222222222222"), Name: "alpha"}},
|
||||
{Project: thoughttypes.Project{ID: uuid.MustParse("33333333-3333-3333-3333-333333333333"), Name: "Beta"}},
|
||||
{Project: thoughttypes.Project{ID: uuid.MustParse("44444444-4444-4444-4444-444444444444"), Name: "beta"}},
|
||||
}
|
||||
|
||||
report := buildDuplicateAuditReport(projects, nil, DuplicateAuditInput{Limit: 1})
|
||||
|
||||
if len(report.ProjectCandidates) != 1 {
|
||||
t.Fatalf("project candidates = %d, want 1", len(report.ProjectCandidates))
|
||||
}
|
||||
if report.Summary.Truncated != true {
|
||||
t.Fatalf("truncated = false, want true")
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,8 @@ type RelatedThought struct {
|
||||
}
|
||||
|
||||
type RelatedOutput struct {
|
||||
Related []RelatedThought `json:"related"`
|
||||
Related []RelatedThought `json:"related"`
|
||||
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||
}
|
||||
|
||||
func NewLinksTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig) *LinksTool {
|
||||
@@ -117,11 +118,13 @@ func (t *LinksTool) Related(ctx context.Context, _ *mcp.CallToolRequest, in Rela
|
||||
includeSemantic = *in.IncludeSemantic
|
||||
}
|
||||
|
||||
var retrievalMode string
|
||||
if includeSemantic {
|
||||
semantic, err := semanticSearch(ctx, t.store, t.embeddings, t.search, thought.Content, t.search.DefaultLimit, t.search.DefaultThreshold, thought.ProjectID, &thought.GUID)
|
||||
semantic, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, thought.Content, t.search.DefaultLimit, t.search.DefaultThreshold, thought.ProjectID, &thought.GUID)
|
||||
if err != nil {
|
||||
return nil, RelatedOutput{}, err
|
||||
}
|
||||
retrievalMode = mode
|
||||
for _, item := range semantic {
|
||||
key := fmt.Sprint(item.ID)
|
||||
if _, ok := seen[key]; ok {
|
||||
@@ -138,5 +141,5 @@ func (t *LinksTool) Related(ctx context.Context, _ *mcp.CallToolRequest, in Rela
|
||||
}
|
||||
}
|
||||
|
||||
return nil, RelatedOutput{Related: related}, nil
|
||||
return nil, RelatedOutput{Related: related, RetrievalMode: retrievalMode}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/session"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type ProjectPersonasTool struct {
|
||||
store *store.DB
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
func NewProjectPersonasTool(db *store.DB, sessions *session.ActiveProjects) *ProjectPersonasTool {
|
||||
return &ProjectPersonasTool{store: db, sessions: sessions}
|
||||
}
|
||||
|
||||
type ProjectPersonaInput struct {
|
||||
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||
PersonaID int64 `json:"persona_id" jsonschema:"persona id"`
|
||||
IsDefault bool `json:"is_default,omitempty" jsonschema:"make this the default persona for the project"`
|
||||
}
|
||||
|
||||
type ProjectPersonaOutput struct {
|
||||
ProjectID int64 `json:"project_id"`
|
||||
PersonaID int64 `json:"persona_id"`
|
||||
}
|
||||
|
||||
type ListProjectPersonasInput struct {
|
||||
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||
}
|
||||
|
||||
type ListProjectPersonasOutput struct {
|
||||
ProjectID int64 `json:"project_id"`
|
||||
Personas []ext.ProjectPersona `json:"personas"`
|
||||
}
|
||||
|
||||
func (t *ProjectPersonasTool) Add(ctx context.Context, req *mcp.CallToolRequest, in ProjectPersonaInput) (*mcp.CallToolResult, ProjectPersonaOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||
if err != nil {
|
||||
return nil, ProjectPersonaOutput{}, err
|
||||
}
|
||||
if err := t.store.AddProjectPersona(ctx, project.NumericID, in.PersonaID, in.IsDefault); err != nil {
|
||||
return nil, ProjectPersonaOutput{}, err
|
||||
}
|
||||
return nil, ProjectPersonaOutput{ProjectID: project.NumericID, PersonaID: in.PersonaID}, nil
|
||||
}
|
||||
|
||||
func (t *ProjectPersonasTool) Remove(ctx context.Context, req *mcp.CallToolRequest, in ProjectPersonaInput) (*mcp.CallToolResult, ProjectPersonaOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||
if err != nil {
|
||||
return nil, ProjectPersonaOutput{}, err
|
||||
}
|
||||
if err := t.store.RemoveProjectPersona(ctx, project.NumericID, in.PersonaID); err != nil {
|
||||
return nil, ProjectPersonaOutput{}, err
|
||||
}
|
||||
return nil, ProjectPersonaOutput{ProjectID: project.NumericID, PersonaID: in.PersonaID}, nil
|
||||
}
|
||||
|
||||
func (t *ProjectPersonasTool) SetDefault(ctx context.Context, req *mcp.CallToolRequest, in ProjectPersonaInput) (*mcp.CallToolResult, ProjectPersonaOutput, error) {
|
||||
in.IsDefault = true
|
||||
return t.Add(ctx, req, in)
|
||||
}
|
||||
|
||||
func (t *ProjectPersonasTool) List(ctx context.Context, req *mcp.CallToolRequest, in ListProjectPersonasInput) (*mcp.CallToolResult, ListProjectPersonasOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||
if err != nil {
|
||||
return nil, ListProjectPersonasOutput{}, err
|
||||
}
|
||||
personas, err := t.store.ListProjectPersonas(ctx, project.NumericID)
|
||||
if err != nil {
|
||||
return nil, ListProjectPersonasOutput{}, err
|
||||
}
|
||||
if personas == nil {
|
||||
personas = []ext.ProjectPersona{}
|
||||
}
|
||||
return nil, ListProjectPersonasOutput{ProjectID: project.NumericID, Personas: personas}, nil
|
||||
}
|
||||
@@ -27,8 +27,9 @@ type RecallInput struct {
|
||||
}
|
||||
|
||||
type RecallOutput struct {
|
||||
Context string `json:"context"`
|
||||
Items []ContextItem `json:"items"`
|
||||
Context string `json:"context"`
|
||||
Items []ContextItem `json:"items"`
|
||||
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||
}
|
||||
|
||||
func NewRecallTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *RecallTool {
|
||||
@@ -53,7 +54,7 @@ func (t *RecallTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in Re
|
||||
projectID = &project.NumericID
|
||||
}
|
||||
|
||||
semantic, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, projectID, nil)
|
||||
semantic, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, projectID, nil)
|
||||
if err != nil {
|
||||
return nil, RecallOutput{}, err
|
||||
}
|
||||
@@ -100,7 +101,8 @@ func (t *RecallTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in Re
|
||||
}
|
||||
|
||||
return nil, RecallOutput{
|
||||
Context: formatContextBlock(header, lines),
|
||||
Items: items,
|
||||
Context: formatContextBlock(header, lines),
|
||||
Items: items,
|
||||
RetrievalMode: mode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -11,10 +11,16 @@ import (
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
const (
|
||||
RetrievalModeSemantic = "semantic"
|
||||
RetrievalModeText = "text"
|
||||
)
|
||||
|
||||
// semanticSearch runs vector similarity search if embeddings exist for the
|
||||
// primary embedding model in the given scope, otherwise falls back to Postgres
|
||||
// full-text search. Search always uses the primary model so query vectors
|
||||
// match rows stored under the primary model name.
|
||||
// It returns the results and the retrieval mode used ("semantic" or "text").
|
||||
func semanticSearch(
|
||||
ctx context.Context,
|
||||
db *store.DB,
|
||||
@@ -25,20 +31,22 @@ func semanticSearch(
|
||||
threshold float64,
|
||||
projectID *int64,
|
||||
excludeID *uuid.UUID,
|
||||
) ([]thoughttypes.SearchResult, error) {
|
||||
) ([]thoughttypes.SearchResult, string, error) {
|
||||
model := embeddings.PrimaryModel()
|
||||
hasEmbeddings, err := db.HasEmbeddingsForModel(ctx, model, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if hasEmbeddings {
|
||||
embedding, err := embeddings.EmbedPrimary(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
return db.SearchSimilarThoughts(ctx, embedding, model, threshold, limit, projectID, excludeID)
|
||||
results, err := db.SearchSimilarThoughts(ctx, embedding, model, threshold, limit, projectID, excludeID)
|
||||
return results, RetrievalModeSemantic, err
|
||||
}
|
||||
|
||||
return db.SearchThoughtsText(ctx, query, limit, projectID, excludeID)
|
||||
results, err := db.SearchThoughtsText(ctx, query, limit, projectID, excludeID)
|
||||
return results, RetrievalModeText, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package tools
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRetrievalModeConstants(t *testing.T) {
|
||||
if RetrievalModeSemantic != "semantic" {
|
||||
t.Fatalf("RetrievalModeSemantic = %q, want %q", RetrievalModeSemantic, "semantic")
|
||||
}
|
||||
if RetrievalModeText != "text" {
|
||||
t.Fatalf("RetrievalModeText = %q, want %q", RetrievalModeText, "text")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchOutputIncludesRetrievalMode(t *testing.T) {
|
||||
out := SearchOutput{RetrievalMode: RetrievalModeSemantic}
|
||||
if out.RetrievalMode != RetrievalModeSemantic {
|
||||
t.Fatalf("SearchOutput.RetrievalMode = %q, want %q", out.RetrievalMode, RetrievalModeSemantic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelatedOutputIncludesRetrievalMode(t *testing.T) {
|
||||
out := RelatedOutput{RetrievalMode: RetrievalModeText}
|
||||
if out.RetrievalMode != RetrievalModeText {
|
||||
t.Fatalf("RelatedOutput.RetrievalMode = %q, want %q", out.RetrievalMode, RetrievalModeText)
|
||||
}
|
||||
// retrieval_mode is omitempty — empty string means semantic search was skipped
|
||||
out2 := RelatedOutput{}
|
||||
if out2.RetrievalMode != "" {
|
||||
t.Fatalf("RelatedOutput.RetrievalMode = %q, want empty when include_semantic is false", out2.RetrievalMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeOutputIncludesRetrievalModeOnlyWhenQueryUsed(t *testing.T) {
|
||||
withQuery := SummarizeOutput{Summary: "s", Count: 1, RetrievalMode: RetrievalModeSemantic}
|
||||
if withQuery.RetrievalMode != RetrievalModeSemantic {
|
||||
t.Fatalf("SummarizeOutput.RetrievalMode = %q, want %q", withQuery.RetrievalMode, RetrievalModeSemantic)
|
||||
}
|
||||
withoutQuery := SummarizeOutput{Summary: "s", Count: 1}
|
||||
if withoutQuery.RetrievalMode != "" {
|
||||
t.Fatalf("SummarizeOutput.RetrievalMode = %q, want empty when no query", withoutQuery.RetrievalMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectContextOutputIncludesRetrievalModeOnlyWhenQueryUsed(t *testing.T) {
|
||||
withQuery := ProjectContextOutput{RetrievalMode: RetrievalModeText}
|
||||
if withQuery.RetrievalMode != RetrievalModeText {
|
||||
t.Fatalf("ProjectContextOutput.RetrievalMode = %q, want %q", withQuery.RetrievalMode, RetrievalModeText)
|
||||
}
|
||||
withoutQuery := ProjectContextOutput{}
|
||||
if withoutQuery.RetrievalMode != "" {
|
||||
t.Fatalf("ProjectContextOutput.RetrievalMode = %q, want empty when no query", withoutQuery.RetrievalMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecallOutputIncludesRetrievalMode(t *testing.T) {
|
||||
out := RecallOutput{RetrievalMode: RetrievalModeSemantic}
|
||||
if out.RetrievalMode != RetrievalModeSemantic {
|
||||
t.Fatalf("RecallOutput.RetrievalMode = %q, want %q", out.RetrievalMode, RetrievalModeSemantic)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,8 @@ type SearchInput struct {
|
||||
}
|
||||
|
||||
type SearchOutput struct {
|
||||
Results []thoughttypes.SearchResult `json:"results"`
|
||||
Results []thoughttypes.SearchResult `json:"results"`
|
||||
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||
}
|
||||
|
||||
func NewSearchTool(db *store.DB, embeddings *ai.EmbeddingRunner, search config.SearchConfig, sessions *session.ActiveProjects) *SearchTool {
|
||||
@@ -55,10 +56,10 @@ func (t *SearchTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in Se
|
||||
_ = t.store.TouchProject(ctx, project.NumericID)
|
||||
}
|
||||
|
||||
results, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, threshold, projectID, nil)
|
||||
results, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, threshold, projectID, nil)
|
||||
if err != nil {
|
||||
return nil, SearchOutput{}, err
|
||||
}
|
||||
|
||||
return nil, SearchOutput{Results: results}, nil
|
||||
return nil, SearchOutput{Results: results, RetrievalMode: mode}, nil
|
||||
}
|
||||
|
||||
@@ -259,8 +259,9 @@ func (t *SkillsTool) GetGuardrail(ctx context.Context, _ *mcp.CallToolRequest, i
|
||||
// add_project_skill
|
||||
|
||||
type AddProjectSkillInput struct {
|
||||
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||
SkillID int64 `json:"skill_id" jsonschema:"skill id to link"`
|
||||
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
||||
SkillID int64 `json:"skill_id" jsonschema:"skill id to link"`
|
||||
Override bool `json:"override,omitempty" jsonschema:"replace a lower-precedence skill with the same name during world-model assembly"`
|
||||
}
|
||||
|
||||
type AddProjectSkillOutput struct {
|
||||
@@ -273,7 +274,7 @@ func (t *SkillsTool) AddProjectSkill(ctx context.Context, req *mcp.CallToolReque
|
||||
if err != nil {
|
||||
return nil, AddProjectSkillOutput{}, err
|
||||
}
|
||||
if err := t.store.AddProjectSkill(ctx, project.NumericID, in.SkillID); err != nil {
|
||||
if err := t.store.AddProjectSkill(ctx, project.NumericID, in.SkillID, in.Override); err != nil {
|
||||
return nil, AddProjectSkillOutput{}, err
|
||||
}
|
||||
return nil, AddProjectSkillOutput{ProjectID: project.NumericID, SkillID: in.SkillID}, nil
|
||||
|
||||
@@ -28,8 +28,9 @@ type SummarizeInput struct {
|
||||
}
|
||||
|
||||
type SummarizeOutput struct {
|
||||
Summary string `json:"summary"`
|
||||
Count int `json:"count"`
|
||||
Summary string `json:"summary"`
|
||||
Count int `json:"count"`
|
||||
RetrievalMode string `json:"retrieval_mode,omitempty"`
|
||||
}
|
||||
|
||||
func NewSummarizeTool(db *store.DB, embeddings *ai.EmbeddingRunner, metadata *ai.MetadataRunner, search config.SearchConfig, sessions *session.ActiveProjects) *SummarizeTool {
|
||||
@@ -47,15 +48,17 @@ func (t *SummarizeTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in
|
||||
lines := make([]string, 0, limit)
|
||||
count := 0
|
||||
|
||||
var retrievalMode string
|
||||
if query != "" {
|
||||
var projectID *int64
|
||||
if project != nil {
|
||||
projectID = &project.NumericID
|
||||
}
|
||||
results, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, projectID, nil)
|
||||
results, mode, err := semanticSearch(ctx, t.store, t.embeddings, t.search, query, limit, t.search.DefaultThreshold, projectID, nil)
|
||||
if err != nil {
|
||||
return nil, SummarizeOutput{}, err
|
||||
}
|
||||
retrievalMode = mode
|
||||
for i, result := range results {
|
||||
lines = append(lines, thoughtContextLine(i, result.Content, result.Metadata, result.Similarity))
|
||||
}
|
||||
@@ -85,5 +88,5 @@ func (t *SummarizeTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in
|
||||
_ = t.store.TouchProject(ctx, project.NumericID)
|
||||
}
|
||||
|
||||
return nil, SummarizeOutput{Summary: summary, Count: count}, nil
|
||||
return nil, SummarizeOutput{Summary: summary, Count: count, RetrievalMode: retrievalMode}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type ThoughtLearningLinksTool struct {
|
||||
store *store.DB
|
||||
}
|
||||
|
||||
type LinkThoughtLearningInput struct {
|
||||
ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought to link"`
|
||||
LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning to link"`
|
||||
Relation string `json:"relation,omitempty" jsonschema:"relationship label, e.g. source, derived_from, related; defaults to source"`
|
||||
}
|
||||
|
||||
type LinkThoughtLearningOutput struct {
|
||||
Linked bool `json:"linked"`
|
||||
}
|
||||
|
||||
type UnlinkThoughtLearningInput struct {
|
||||
ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought"`
|
||||
LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning"`
|
||||
}
|
||||
|
||||
type UnlinkThoughtLearningOutput struct {
|
||||
Unlinked bool `json:"unlinked"`
|
||||
}
|
||||
|
||||
type GetThoughtLearningsInput struct {
|
||||
ThoughtID string `json:"thought_id" jsonschema:"UUID of the thought"`
|
||||
}
|
||||
|
||||
type GetThoughtLearningsOutput struct {
|
||||
Learnings []thoughttypes.LinkedLearning `json:"learnings"`
|
||||
}
|
||||
|
||||
type GetLearningThoughtsInput struct {
|
||||
LearningID int64 `json:"learning_id" jsonschema:"numeric id of the learning"`
|
||||
}
|
||||
|
||||
type GetLearningThoughtsOutput struct {
|
||||
Thoughts []thoughttypes.LinkedLearningThought `json:"thoughts"`
|
||||
}
|
||||
|
||||
func NewThoughtLearningLinksTool(db *store.DB) *ThoughtLearningLinksTool {
|
||||
return &ThoughtLearningLinksTool{store: db}
|
||||
}
|
||||
|
||||
func (t *ThoughtLearningLinksTool) Link(ctx context.Context, _ *mcp.CallToolRequest, in LinkThoughtLearningInput) (*mcp.CallToolResult, LinkThoughtLearningOutput, error) {
|
||||
if err := t.ensureConfigured(); err != nil {
|
||||
return nil, LinkThoughtLearningOutput{}, err
|
||||
}
|
||||
|
||||
thoughtGUID, err := parseUUID(in.ThoughtID)
|
||||
if err != nil {
|
||||
return nil, LinkThoughtLearningOutput{}, err
|
||||
}
|
||||
if in.LearningID <= 0 {
|
||||
return nil, LinkThoughtLearningOutput{}, errRequiredField("learning_id")
|
||||
}
|
||||
relation := strings.TrimSpace(in.Relation)
|
||||
if relation == "" {
|
||||
relation = "source"
|
||||
}
|
||||
|
||||
if _, err := t.store.GetThought(ctx, thoughtGUID); err != nil {
|
||||
return nil, LinkThoughtLearningOutput{}, errEntityNotFound("thought", "thought_id", in.ThoughtID)
|
||||
}
|
||||
if _, err := t.store.GetLearning(ctx, in.LearningID); err != nil {
|
||||
return nil, LinkThoughtLearningOutput{}, errEntityNotFound("learning", "learning_id", strconv.FormatInt(in.LearningID, 10))
|
||||
}
|
||||
|
||||
if err := t.store.LinkThoughtLearning(ctx, thoughtGUID, in.LearningID, relation); err != nil {
|
||||
return nil, LinkThoughtLearningOutput{}, err
|
||||
}
|
||||
return nil, LinkThoughtLearningOutput{Linked: true}, nil
|
||||
}
|
||||
|
||||
func (t *ThoughtLearningLinksTool) Unlink(ctx context.Context, _ *mcp.CallToolRequest, in UnlinkThoughtLearningInput) (*mcp.CallToolResult, UnlinkThoughtLearningOutput, error) {
|
||||
if err := t.ensureConfigured(); err != nil {
|
||||
return nil, UnlinkThoughtLearningOutput{}, err
|
||||
}
|
||||
|
||||
thoughtGUID, err := parseUUID(in.ThoughtID)
|
||||
if err != nil {
|
||||
return nil, UnlinkThoughtLearningOutput{}, err
|
||||
}
|
||||
if in.LearningID <= 0 {
|
||||
return nil, UnlinkThoughtLearningOutput{}, errRequiredField("learning_id")
|
||||
}
|
||||
|
||||
if err := t.store.UnlinkThoughtLearning(ctx, thoughtGUID, in.LearningID); err != nil {
|
||||
return nil, UnlinkThoughtLearningOutput{}, err
|
||||
}
|
||||
return nil, UnlinkThoughtLearningOutput{Unlinked: true}, nil
|
||||
}
|
||||
|
||||
func (t *ThoughtLearningLinksTool) GetThoughtLearnings(ctx context.Context, _ *mcp.CallToolRequest, in GetThoughtLearningsInput) (*mcp.CallToolResult, GetThoughtLearningsOutput, error) {
|
||||
if err := t.ensureConfigured(); err != nil {
|
||||
return nil, GetThoughtLearningsOutput{}, err
|
||||
}
|
||||
|
||||
thoughtGUID, err := parseUUID(in.ThoughtID)
|
||||
if err != nil {
|
||||
return nil, GetThoughtLearningsOutput{}, err
|
||||
}
|
||||
|
||||
if _, err := t.store.GetThought(ctx, thoughtGUID); err != nil {
|
||||
return nil, GetThoughtLearningsOutput{}, errEntityNotFound("thought", "thought_id", in.ThoughtID)
|
||||
}
|
||||
|
||||
learnings, err := t.store.GetLinkedLearnings(ctx, thoughtGUID)
|
||||
if err != nil {
|
||||
return nil, GetThoughtLearningsOutput{}, err
|
||||
}
|
||||
return nil, GetThoughtLearningsOutput{Learnings: learnings}, nil
|
||||
}
|
||||
|
||||
func (t *ThoughtLearningLinksTool) GetLearningThoughts(ctx context.Context, _ *mcp.CallToolRequest, in GetLearningThoughtsInput) (*mcp.CallToolResult, GetLearningThoughtsOutput, error) {
|
||||
if err := t.ensureConfigured(); err != nil {
|
||||
return nil, GetLearningThoughtsOutput{}, err
|
||||
}
|
||||
|
||||
if in.LearningID <= 0 {
|
||||
return nil, GetLearningThoughtsOutput{}, errRequiredField("learning_id")
|
||||
}
|
||||
|
||||
if _, err := t.store.GetLearning(ctx, in.LearningID); err != nil {
|
||||
return nil, GetLearningThoughtsOutput{}, errEntityNotFound("learning", "learning_id", strconv.FormatInt(in.LearningID, 10))
|
||||
}
|
||||
|
||||
thoughts, err := t.store.GetLinkedThoughtsForLearning(ctx, in.LearningID)
|
||||
if err != nil {
|
||||
return nil, GetLearningThoughtsOutput{}, err
|
||||
}
|
||||
return nil, GetLearningThoughtsOutput{Thoughts: thoughts}, nil
|
||||
}
|
||||
|
||||
func (t *ThoughtLearningLinksTool) ensureConfigured() error {
|
||||
if t == nil || t.store == nil {
|
||||
return errInvalidInput("thought learning links tool is not configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/session"
|
||||
"git.warky.dev/wdevs/amcs/internal/store"
|
||||
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
type WorldModelTool struct {
|
||||
store *store.DB
|
||||
sessions *session.ActiveProjects
|
||||
}
|
||||
|
||||
func NewWorldModelTool(db *store.DB, sessions *session.ActiveProjects) *WorldModelTool {
|
||||
return &WorldModelTool{store: db, sessions: sessions}
|
||||
}
|
||||
|
||||
type BootstrapWorldModelInput struct {
|
||||
Project string `json:"project" jsonschema:"project name or id; must be explicit for deterministic startup"`
|
||||
ContextLimit int `json:"context_limit,omitempty" jsonschema:"recent project thoughts to include (default 10, maximum 50)"`
|
||||
}
|
||||
|
||||
type EffectiveSkill struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
Override bool `json:"override"`
|
||||
}
|
||||
|
||||
type WorldModelContextItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Metadata ext.ThoughtMetadata `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type BootstrapWorldModelOutput struct {
|
||||
Version int `json:"version"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Project ext.Project `json:"project"`
|
||||
Skills []EffectiveSkill `json:"skills"`
|
||||
Guardrails []ext.AgentGuardrail `json:"guardrails"`
|
||||
Personas []ext.ProjectPersona `json:"personas"`
|
||||
PersonaManifests []ext.PersonaManifest `json:"persona_manifests"`
|
||||
DefaultPersona *ext.PersonaFull `json:"default_persona,omitempty"`
|
||||
Context []WorldModelContextItem `json:"context"`
|
||||
}
|
||||
|
||||
func (t *WorldModelTool) Bootstrap(ctx context.Context, req *mcp.CallToolRequest, in BootstrapWorldModelInput) (*mcp.CallToolResult, BootstrapWorldModelOutput, error) {
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, true)
|
||||
if err != nil {
|
||||
return nil, BootstrapWorldModelOutput{}, err
|
||||
}
|
||||
if t.sessions != nil && req != nil && req.Session != nil {
|
||||
t.sessions.Set(req.Session.ID(), project.ID)
|
||||
}
|
||||
|
||||
projectSkills, err := t.store.ListProjectSkills(ctx, project.NumericID)
|
||||
if err != nil {
|
||||
return nil, BootstrapWorldModelOutput{}, err
|
||||
}
|
||||
guardrails, err := t.store.ListProjectGuardrails(ctx, project.NumericID)
|
||||
if err != nil {
|
||||
return nil, BootstrapWorldModelOutput{}, err
|
||||
}
|
||||
personas, err := t.store.ListProjectPersonas(ctx, project.NumericID)
|
||||
if err != nil {
|
||||
return nil, BootstrapWorldModelOutput{}, err
|
||||
}
|
||||
|
||||
manifests := make([]ext.PersonaManifest, 0, len(personas))
|
||||
for _, linked := range personas {
|
||||
manifest, err := t.store.GetPersonaManifest(ctx, linked.Persona.Name)
|
||||
if err != nil {
|
||||
return nil, BootstrapWorldModelOutput{}, err
|
||||
}
|
||||
manifests = append(manifests, manifest)
|
||||
}
|
||||
|
||||
var defaultPersona *ext.PersonaFull
|
||||
for _, linked := range personas {
|
||||
if !linked.IsDefault {
|
||||
continue
|
||||
}
|
||||
loaded, err := t.store.GetPersona(ctx, linked.Persona.Name, true, nil)
|
||||
if err != nil {
|
||||
return nil, BootstrapWorldModelOutput{}, err
|
||||
}
|
||||
defaultPersona = &loaded
|
||||
break
|
||||
}
|
||||
|
||||
skills := mergeWorldModelSkills(projectSkills, defaultPersona)
|
||||
guardrails = mergeWorldModelGuardrails(guardrails, defaultPersona)
|
||||
limit := in.ContextLimit
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 50 {
|
||||
limit = 50
|
||||
}
|
||||
recent, err := t.store.RecentThoughts(ctx, &project.NumericID, limit, 0)
|
||||
if err != nil {
|
||||
return nil, BootstrapWorldModelOutput{}, err
|
||||
}
|
||||
contextItems := make([]WorldModelContextItem, 0, len(recent))
|
||||
for _, thought := range recent {
|
||||
contextItems = append(contextItems, WorldModelContextItem{ID: thought.ID, Content: thought.Content, Metadata: thought.Metadata, CreatedAt: thought.CreatedAt})
|
||||
}
|
||||
if guardrails == nil {
|
||||
guardrails = []ext.AgentGuardrail{}
|
||||
}
|
||||
if personas == nil {
|
||||
personas = []ext.ProjectPersona{}
|
||||
}
|
||||
_ = t.store.TouchProject(ctx, project.NumericID)
|
||||
|
||||
return nil, BootstrapWorldModelOutput{
|
||||
Version: 1, GeneratedAt: time.Now().UTC(), Project: *project, Skills: skills,
|
||||
Guardrails: guardrails, Personas: personas, PersonaManifests: manifests,
|
||||
DefaultPersona: defaultPersona, Context: contextItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mergeWorldModelSkills(projectSkills []ext.AgentSkill, persona *ext.PersonaFull) []EffectiveSkill {
|
||||
result := make([]EffectiveSkill, 0, len(projectSkills))
|
||||
index := make(map[string]int, len(projectSkills))
|
||||
for _, skill := range projectSkills {
|
||||
key := strings.ToLower(skill.Name)
|
||||
index[key] = len(result)
|
||||
result = append(result, EffectiveSkill{ID: skill.ID, Name: skill.Name, Description: skill.Description, Content: skill.Content, Tags: skill.Tags, Source: "project", Override: skill.Override})
|
||||
}
|
||||
if persona == nil {
|
||||
return result
|
||||
}
|
||||
for _, skill := range persona.Skills {
|
||||
key := strings.ToLower(skill.Name)
|
||||
entry := EffectiveSkill{ID: skill.ID, Name: skill.Name, Description: skill.Description, Content: skill.Content, Tags: skill.Tags, Source: "persona", Override: skill.Override}
|
||||
if i, ok := index[key]; ok {
|
||||
if skill.Override {
|
||||
result[i] = entry
|
||||
}
|
||||
continue
|
||||
}
|
||||
index[key] = len(result)
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeWorldModelGuardrails(project []ext.AgentGuardrail, persona *ext.PersonaFull) []ext.AgentGuardrail {
|
||||
result := append([]ext.AgentGuardrail(nil), project...)
|
||||
index := make(map[string]int, len(result))
|
||||
for i, guardrail := range result {
|
||||
index[strings.ToLower(guardrail.Name)] = i
|
||||
}
|
||||
if persona == nil {
|
||||
return result
|
||||
}
|
||||
severity := map[string]int{"low": 1, "medium": 2, "high": 3, "critical": 4}
|
||||
for _, guardrail := range persona.Guardrails {
|
||||
key := strings.ToLower(guardrail.Name)
|
||||
if i, ok := index[key]; ok {
|
||||
if severity[guardrail.Severity] > severity[result[i].Severity] {
|
||||
result[i].Severity = guardrail.Severity
|
||||
result[i].Content = guardrail.Content
|
||||
}
|
||||
continue
|
||||
}
|
||||
index[key] = len(result)
|
||||
result = append(result, ext.AgentGuardrail{ID: guardrail.ID, Name: guardrail.Name, Description: guardrail.Description, Content: guardrail.Content, Severity: guardrail.Severity, Tags: guardrail.Tags})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
|
||||
func TestMergeWorldModelSkills(t *testing.T) {
|
||||
project := []ext.AgentSkill{
|
||||
{ID: 1, Name: "go", Content: "project", Tags: []string{}},
|
||||
{ID: 2, Name: "postgres", Content: "database", Tags: []string{}},
|
||||
}
|
||||
persona := &ext.PersonaFull{Skills: []ext.PersonaSkillEntry{
|
||||
{ID: 1, Name: "go", Content: "ignored additive duplicate", Tags: []string{}},
|
||||
{ID: 3, Name: "Postgres", Content: "persona override", Tags: []string{}, Override: true},
|
||||
{ID: 4, Name: "writing", Content: "additive", Tags: []string{}},
|
||||
}}
|
||||
|
||||
got := mergeWorldModelSkills(project, persona)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("len(skills) = %d, want 3", len(got))
|
||||
}
|
||||
if got[0].Content != "project" || got[0].Source != "project" {
|
||||
t.Fatalf("additive duplicate replaced project skill: %#v", got[0])
|
||||
}
|
||||
if got[1].Content != "persona override" || got[1].Source != "persona" || !got[1].Override {
|
||||
t.Fatalf("override did not replace project skill: %#v", got[1])
|
||||
}
|
||||
if got[2].Name != "writing" || got[2].Source != "persona" {
|
||||
t.Fatalf("new additive persona skill missing: %#v", got[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeWorldModelGuardrailsKeepsStrictest(t *testing.T) {
|
||||
project := []ext.AgentGuardrail{{ID: 1, Name: "safe", Severity: "high", Content: "project"}}
|
||||
persona := &ext.PersonaFull{Guardrails: []ext.PersonaGuardrailEntry{
|
||||
{ID: 2, Name: "Safe", Severity: "low", Content: "weaker"},
|
||||
{ID: 3, Name: "privacy", Severity: "critical", Content: "new"},
|
||||
}}
|
||||
|
||||
got := mergeWorldModelGuardrails(project, persona)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len(guardrails) = %d, want 2", len(got))
|
||||
}
|
||||
if got[0].Severity != "high" || got[0].Content != "project" {
|
||||
t.Fatalf("persona weakened project guardrail: %#v", got[0])
|
||||
}
|
||||
if got[1].Name != "privacy" || got[1].Severity != "critical" {
|
||||
t.Fatalf("new persona guardrail missing: %#v", got[1])
|
||||
}
|
||||
}
|
||||
@@ -81,17 +81,17 @@ type ArcStage struct {
|
||||
// 2. active arc-stage parts
|
||||
// 3. persona-linked parts (base)
|
||||
type PersonaFull struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Body string `json:"body"` // summary or detail, depending on mode
|
||||
CompiledSummary string `json:"compiled_summary,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
Parts []AssembledPart `json:"parts"`
|
||||
Skills []PersonaSkillEntry `json:"skills"`
|
||||
Guardrails []PersonaGuardrailEntry `json:"guardrails"`
|
||||
Traits []PersonaTraitEntry `json:"traits"`
|
||||
Arc *PersonaArcState `json:"arc,omitempty"`
|
||||
Detail bool `json:"detail"` // whether full detail was requested
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Body string `json:"body"` // summary or detail, depending on mode
|
||||
CompiledSummary string `json:"compiled_summary,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
Parts []AssembledPart `json:"parts"`
|
||||
Skills []PersonaSkillEntry `json:"skills"`
|
||||
Guardrails []PersonaGuardrailEntry `json:"guardrails"`
|
||||
Traits []PersonaTraitEntry `json:"traits"`
|
||||
Arc *PersonaArcState `json:"arc,omitempty"`
|
||||
Detail bool `json:"detail"` // whether full detail was requested
|
||||
}
|
||||
|
||||
type AssembledPart struct {
|
||||
@@ -109,13 +109,14 @@ type PersonaSkillEntry struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Content string `json:"content,omitempty"` // only when detail=true
|
||||
Tags []string `json:"tags"`
|
||||
Override bool `json:"override,omitempty"`
|
||||
}
|
||||
|
||||
type PersonaGuardrailEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Content string `json:"content,omitempty"` // only when detail=true
|
||||
Content string `json:"content,omitempty"` // only when detail=true
|
||||
Severity string `json:"severity,omitempty"` // only when detail=true
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
@@ -168,6 +169,12 @@ type ManifestSkill struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Override bool `json:"override,omitempty"`
|
||||
}
|
||||
|
||||
type ProjectPersona struct {
|
||||
Persona Persona `json:"persona"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type ManifestGuardrail struct {
|
||||
|
||||
@@ -229,6 +229,7 @@ type AgentSkill struct {
|
||||
DomainTags []string `json:"domain_tags"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Override bool `json:"override,omitempty"`
|
||||
}
|
||||
|
||||
type AgentGuardrail struct {
|
||||
|
||||
@@ -39,3 +39,22 @@ type LinkedThought struct {
|
||||
Direction string `json:"direction"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type ThoughtLearningLink struct {
|
||||
ThoughtID int64 `json:"thought_id"`
|
||||
LearningID int64 `json:"learning_id"`
|
||||
Relation string `json:"relation"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type LinkedLearning struct {
|
||||
Learning Learning `json:"learning"`
|
||||
Relation string `json:"relation"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type LinkedLearningThought struct {
|
||||
Thought Thought `json:"thought"`
|
||||
Relation string `json:"relation"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
+21
-13
@@ -14,12 +14,20 @@ type ThoughtMetadata struct {
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Attachments []ThoughtAttachment `json:"attachments,omitempty"`
|
||||
Webhook *WebhookMetadata `json:"webhook,omitempty"`
|
||||
MetadataStatus string `json:"metadata_status,omitempty"`
|
||||
MetadataUpdatedAt string `json:"metadata_updated_at,omitempty"`
|
||||
MetadataLastAttemptedAt string `json:"metadata_last_attempted_at,omitempty"`
|
||||
MetadataError string `json:"metadata_error,omitempty"`
|
||||
}
|
||||
|
||||
type WebhookMetadata struct {
|
||||
ReceivedAt string `json:"received_at"`
|
||||
IDempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
ExternalID string `json:"external_id,omitempty"`
|
||||
SourceMetadata map[string]any `json:"source_metadata,omitempty"`
|
||||
}
|
||||
|
||||
type ThoughtAttachment struct {
|
||||
FileID uuid.UUID `json:"file_id"`
|
||||
Name string `json:"name"`
|
||||
@@ -30,19 +38,19 @@ type ThoughtAttachment struct {
|
||||
}
|
||||
|
||||
type StoredFile struct {
|
||||
ID int64 `json:"id"`
|
||||
GUID uuid.UUID `json:"guid"`
|
||||
ThoughtID *int64 `json:"thought_id,omitempty"`
|
||||
ProjectID *int64 `json:"project_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
MediaType string `json:"media_type"`
|
||||
Kind string `json:"kind"`
|
||||
Encoding string `json:"encoding"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Content []byte `json:"-"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
GUID uuid.UUID `json:"guid"`
|
||||
ThoughtID *int64 `json:"thought_id,omitempty"`
|
||||
ProjectID *int64 `json:"project_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
MediaType string `json:"media_type"`
|
||||
Kind string `json:"kind"`
|
||||
Encoding string `json:"encoding"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Content []byte `json:"-"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type StoredFileFilter struct {
|
||||
|
||||
@@ -5,4 +5,7 @@ import _ "embed"
|
||||
var (
|
||||
//go:embed memory.md
|
||||
MemoryInstructions []byte
|
||||
|
||||
//go:embed world_model_intro.md
|
||||
WorldModelIntro []byte
|
||||
)
|
||||
|
||||
+16
-3
@@ -28,7 +28,20 @@ Use AMCS as memory with two scopes:
|
||||
|
||||
At the very start of any session with AMCS:
|
||||
|
||||
1. Call `describe_tools` to get the full list of available tools with their categories and any notes you have previously annotated. Read the notes before using a tool — they contain accumulated gotchas, workflow patterns, and field-ordering requirements you have recorded from prior sessions.
|
||||
1. Read the `amcs://world-model/intro` resource.
|
||||
2. Identify the current project and call `bootstrap_world_model` with its explicit name or ID and the smallest useful `context_limit`.
|
||||
3. Apply the returned skills and guardrails, but keep persona manifests and memory summaries as indexes. Load detailed content only when the current task requires it.
|
||||
4. Call `describe_tools` only when tool discovery or saved usage notes are needed. Prefer a category filter instead of loading the full catalog.
|
||||
|
||||
## Token Budget and Progressive Loading
|
||||
|
||||
- Minimize startup and working context. Do not load data merely because it is available.
|
||||
- Start with names, summaries, manifests, and metadata. Fetch full skill, guardrail, persona, trait, part, thought, plan, learning, chat, or file content only when it is relevant to the current task.
|
||||
- Use the smallest practical `limit` or `context_limit`, narrow queries, and project/category filters. Increase them only when the initial result is insufficient.
|
||||
- Do not preload every persona, skill, guardrail, plan, file, or historical memory. Follow references on demand.
|
||||
- Avoid repeating unchanged world-model content in the conversation. Keep a concise working summary and refresh only when the project or task changes, or when stale context is suspected.
|
||||
- Prefer `get_persona_manifest` and compiled summaries before `get_agent_persona(detail=true)`. Prefer `list_skills` metadata before `get_skill`, and file metadata before `load_file`.
|
||||
- Guardrails and directly applicable skills are mandatory even under a tight token budget; token minimization must never omit an applicable constraint.
|
||||
|
||||
## Project Session Startup
|
||||
|
||||
@@ -51,7 +64,7 @@ Do not abandon the project scope or retry without a project. The project simply
|
||||
## Project Memory Rules
|
||||
|
||||
- Use project memory for code decisions, architecture, TODOs, debugging findings, and context specific to the current repo or workstream.
|
||||
- Before substantial work, always retrieve context with `get_project_context` or `recall_context` so prior decisions inform your approach.
|
||||
- Before substantial work, retrieve focused context with `get_project_context` or `recall_context` only when prior decisions may affect the task. Use a narrow query and small limit first.
|
||||
- Save durable project facts with `capture_thought` after completing meaningful work.
|
||||
- Use structured learnings for curated, reusable lessons that should remain distinct from raw thought capture.
|
||||
- Use `save_file` or `upload_file` for project assets the memory should retain, such as screenshots, PDFs, audio notes, and other documents.
|
||||
@@ -135,4 +148,4 @@ Notes are returned by `describe_tools` in future sessions. Annotate whenever you
|
||||
|
||||
## Short Operational Form
|
||||
|
||||
At the start of every session, call `describe_tools` to read the full tool list and any accumulated usage notes. Use AMCS memory in project scope when the current work matches a known project; if no clear project matches, global notebook memory is allowed for non-project-specific information. At the start of every project session call `list_project_skills` and `list_project_guardrails` and apply what is returned; only create new skills or guardrails if none exist. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`. Store raw/durable notes with `capture_thought`, store curated durable lessons with `add_learning`, and track structured multi-step goals with `create_plan`. Use `get_plan` to load a plan's full context including dependencies, related plans, and linked skills/guardrails. Stamp `last_reviewed_at` on plans you review with `update_plan mark_reviewed: true`. For binary files or files larger than 10 MB, call `upload_file` with `content_path` to stage the file and get an `amcs://files/{id}` URI, then pass that URI to `save_file` as `content_uri` to link it to a thought. For small files, use `save_file` or `upload_file` with `content_base64` directly. Browse stored files with `list_files`, and load them with `load_file` only when their contents are needed. Stored files can also be read as raw binary via MCP resources at `amcs://files/{id}`. Never store project-specific memory globally when a matching project exists, and never store memory in the wrong project. If project matching is ambiguous, ask the user. If a tool returns `project_not_found`, call `create_project` with that name and retry — never drop the project scope. Whenever you discover a non-obvious tool behaviour, gotcha, or workflow pattern, record it with `annotate_tool` so future sessions benefit.
|
||||
At session start, identify the project and call `bootstrap_world_model` with a small `context_limit`. Apply mandatory skills and guardrails, but otherwise use summaries and manifests as indexes and load details only when needed. Call `describe_tools` only for discovery or saved notes, preferably with a category filter. Use project scope when the work matches a known project; pass `project` explicitly for stateless clients. Store durable notes with `capture_thought`, curated lessons with `add_learning`, and multi-step work with `create_plan`. Retrieve plans, memories, personas, and files on demand using narrow queries and the smallest useful limits. Never store memory in the wrong scope, silently choose an ambiguous project, or omit an applicable guardrail to save tokens. Record non-obvious tool behavior with `annotate_tool`.
|
||||
|
||||
+99
@@ -1,5 +1,104 @@
|
||||
# AMCS TODO
|
||||
|
||||
## Memory Consolidation and Retrieval Quality
|
||||
|
||||
No prior consolidation implementation notes were found in the project. This roadmap starts clean and follows AMCS's current Go, PostgreSQL/pgvector, thoughts, learnings, projects, and model-specific embeddings architecture.
|
||||
|
||||
### Current-shape decisions
|
||||
|
||||
- Use bigint thought IDs for foreign keys and queue references, matching the current schema. Keep thought GUIDs as external identifiers rather than introducing UUID foreign keys without a specific interoperability need.
|
||||
- Embeddings are stored in the separate, model-specific `embeddings` table. Similarity queries and indexes must include the embedding model and project scope where applicable.
|
||||
- Verify the existing pgvector index strategy before adding another HNSW index. Add or change an index only when the current generated schema and query plan show it is needed.
|
||||
- All consolidation writes are soft, auditable, idempotent, and reversible.
|
||||
|
||||
### Phase 0 - Schema additions
|
||||
|
||||
Add to `thoughts`:
|
||||
|
||||
- `confidence smallint`
|
||||
- `source text`
|
||||
- `valid_until timestamptz`
|
||||
- `superseded_by bigint` referencing `thoughts.id`
|
||||
- `salience real`
|
||||
- `outcome text` constrained to `confirmed`, `refuted`, or null
|
||||
|
||||
Add tables:
|
||||
|
||||
- `consolidation_queue(id, kind, thought_a, thought_b, status, created_at)`
|
||||
- `consolidation_audit(id, action, before jsonb, after jsonb, actor, ts)`
|
||||
|
||||
The audit record must contain enough information to reverse every consolidation mutation. Define queue uniqueness and status transitions so retries cannot enqueue or process the same pair twice.
|
||||
|
||||
Confidence must be available through the existing admin thought resource. Return both the normalized numeric value and enough provenance to explain whether it was user-supplied, heuristic, outcome-derived, or consolidation-derived. Existing thoughts need a documented neutral default or backfill policy rather than silently appearing maximally confident.
|
||||
|
||||
### Phase 1 - Algorithmic candidate generation
|
||||
|
||||
Run nightly SQL candidate generation without an LLM:
|
||||
|
||||
- Near duplicates: self-join model-compatible embeddings and enqueue canonical thought pairs where `1 - (a.embedding <=> b.embedding) > 0.92`.
|
||||
- Contradiction suspects: combine high cosine similarity with low `pg_trgm` `similarity()` and enqueue as `kind = 'contradiction'`.
|
||||
- Decay: demote thoughts past `valid_until`; apply recency weighting during retrieval rather than rewriting content.
|
||||
|
||||
Candidate queries must de-duplicate ordered pairs, exclude already superseded thoughts, respect project boundaries, and avoid comparing embeddings from different models.
|
||||
|
||||
### Phase 2 - Retrieval ranking (ship first)
|
||||
|
||||
Change `recall_context` from cosine-only top-k ranking to a bounded composite score based on:
|
||||
|
||||
```text
|
||||
cosine_similarity * exp(-lambda * age_days) * confidence_weight * outcome_weight
|
||||
```
|
||||
|
||||
Exclude superseded and low-salience thoughts from default recall, with explicit options for diagnostic retrieval. Tune defaults conservatively and preserve the current full-text fallback when embeddings are unavailable.
|
||||
|
||||
This phase should ship first because it provides the largest recall-quality improvement without requiring consolidation writes or an adjudication model.
|
||||
|
||||
### Phase 3 - LLM adjudication worker
|
||||
|
||||
Add a resumable Go worker that claims queued pairs and calls a low-cost configured provider only for flagged candidates. Support two structured prompts:
|
||||
|
||||
- Merge: produce one normalized schema entry from genuine duplicates.
|
||||
- Resolve conflict: verify a real contradiction and select or synthesize the supported result.
|
||||
|
||||
Writes remain soft: set `superseded_by`, never delete source thoughts, and append every mutation to `consolidation_audit`. Queue claiming, provider calls, and writes must be idempotent and safe across restarts.
|
||||
|
||||
### Phase 4 - Salience gate
|
||||
|
||||
Compute initial salience during `capture_thought` using explainable heuristics such as length, code presence, decision verbs, and thought type. Store low-salience thoughts but exclude them from default recall. Consider a logistic model only after measured retrieval outcomes show the heuristics have plateaued.
|
||||
|
||||
### Phase 5 - Outcome loop
|
||||
|
||||
Add a tool or update flag that marks a thought as `confirmed` or `refuted` when its decision plays out. Preserve the change in the consolidation audit and feed the outcome into retrieval ranking as the real-world error signal.
|
||||
|
||||
### Phase 6 - Admin confidence and SQL job observability
|
||||
|
||||
Extend the Svelte admin UI and its ResolveSpec-compatible backend resources with two read-oriented views.
|
||||
|
||||
#### Thought confidence
|
||||
|
||||
- Show confidence in thought list and detail views without requiring the operator to open raw metadata.
|
||||
- Display the numeric value, a human-readable band, provenance, outcome, salience, validity window, and superseded state.
|
||||
- Allow sorting and filtering by confidence, outcome, validity, and superseded state.
|
||||
- Visually distinguish unknown/unscored confidence from low confidence; never coerce null into zero or one.
|
||||
- Explain ranking inputs in the detail view so operators can understand why a thought was included or excluded from default recall.
|
||||
- Keep confidence mutation behind the normal thought update/audit path rather than allowing unaudited inline table edits.
|
||||
|
||||
#### Consolidation SQL jobs
|
||||
|
||||
- Show configured `pg_cron` consolidation jobs and their schedule, enabled state, last start/end time, duration, status, and sanitized result/error message.
|
||||
- Show currently running consolidation jobs separately from recent completed/failed runs using `cron.job` and `cron.job_run_details` through a permission-limited backend projection.
|
||||
- Include queue counts by status and consolidation kind so a successful cron invocation cannot hide a stalled or growing queue.
|
||||
- Add filters for job, status, and time range, plus clear empty/unavailable states when `pg_cron` is not installed or its views are inaccessible.
|
||||
- Do not expose raw SQL command text, connection strings, credentials, or unrestricted `pg_stat_activity` data.
|
||||
- Keep the first version read-only. Manual run, retry, pause, or cancel controls require separate authorization, audit logging, and explicit confirmation.
|
||||
- Poll conservatively only while the jobs screen is visible; stop polling when hidden and avoid loading run history during normal admin startup.
|
||||
|
||||
Backend tests must cover `pg_cron` unavailable/permission-denied behavior, sanitized errors, running-versus-finished classification, pagination, and project-independent admin authorization. Frontend tests must cover confidence null handling, failed/running job states, queue backlog warnings, and polling cleanup.
|
||||
|
||||
### Delivery principle
|
||||
|
||||
SQL handles high-volume candidate recall and ranking; the LLM handles low-volume precision decisions for a bounded number of queued pairs. Implement Phase 2 first, then Phase 0/1 foundations needed for consolidation, followed by the worker, salience gate, outcome feedback loop, and admin observability. Confidence fields should appear in the admin UI as soon as Phase 0 lands; SQL job observability should land with the first scheduled Phase 1 jobs.
|
||||
|
||||
## Future Plugin: Lifestyle Tools (calendar, meals, household, CRM)
|
||||
|
||||
The following tool groups have been removed from the core server and are candidates for a separate optional plugin or extension server. The store/tool implementations remain in the codebase but are no longer registered.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# AMCS World Model
|
||||
|
||||
AMCS provides a project-scoped world model containing memory, skills, guardrails, personas, and recent context.
|
||||
|
||||
At the start of an agent or chat session:
|
||||
|
||||
1. Identify the current project from explicit user input or the workspace.
|
||||
2. Call `list_projects` when the project has not already been confirmed.
|
||||
3. Call `bootstrap_world_model` with the explicit project name or ID.
|
||||
4. Apply every returned guardrail and effective skill before continuing.
|
||||
5. Use the default persona when present. Other linked persona manifests describe personas available on demand.
|
||||
|
||||
Never silently choose or create a project. If no project matches, ask the user or create one only with explicit approval. Stateless clients must continue passing the project explicitly on project-scoped calls.
|
||||
|
||||
Skills are additive by default. A linked skill with `override: true` replaces a lower-precedence skill with the same stable name. Guardrails are additive and later layers cannot weaken them.
|
||||
+2203
-89
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user