Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d4e6d0939 | |||
| f94fddddb1 | |||
| 631bb64109 | |||
| 4f8f2f4190 | |||
| 1689167a7b | |||
| 5cd81f325f | |||
| cd010fc7a1 | |||
| e3a4a3c5c7 | |||
| 4b3f0b1b55 | |||
| e459775740 | |||
| 5718685c40 | |||
| 5c899d1635 | |||
| 8db2141d45 | |||
| 3198600031 | |||
| 81a3470407 | |||
| cfc78e0493 | |||
| c179e014ad |
@@ -18,6 +18,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: '1.26'
|
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
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -31,9 +39,37 @@ jobs:
|
|||||||
- name: Download dependencies
|
- name: Download dependencies
|
||||||
run: go mod download
|
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
|
- name: Tidy modules
|
||||||
run: go mod tidy
|
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
|
- name: Run tests
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|
||||||
|
|||||||
@@ -36,3 +36,4 @@ ui/.svelte-kit/
|
|||||||
internal/app/ui/dist/*
|
internal/app/ui/dist/*
|
||||||
!internal/app/ui/dist/placeholder.txt
|
!internal/app/ui/dist/placeholder.txt
|
||||||
.codex
|
.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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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
|
||||||
|
|
||||||
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.
|
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
|
## 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
|
### 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:
|
Link existing skills and guardrails to a project so they are automatically available when that project is active:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "project": "my-project", "skill_id": "<uuid>" }
|
{ "project": "my-project", "skill_id": 42, "override": false }
|
||||||
{ "project": "my-project", "guardrail_id": "<uuid>" }
|
{ "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
|
## Configuration
|
||||||
|
|
||||||
Config is YAML-driven. Copy `configs/config.example.yaml` and set:
|
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
|
### 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
|
### HTTP upload and download
|
||||||
|
|
||||||
@@ -471,7 +510,7 @@ metadata_retry:
|
|||||||
include_archived: false
|
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
|
## Client Setup
|
||||||
|
|
||||||
|
|||||||
@@ -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,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_key` 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_key` 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_key`.
|
||||||
|
2. OAuth bearer token: resolve token through `TokenStore.Lookup`; use returned client id/key id as `tenant_key`.
|
||||||
|
3. OAuth Basic client credentials: resolve through `OAuthRegistry.Lookup`; use returned client id as `tenant_key`.
|
||||||
|
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_key text` to all user-owned tables:
|
||||||
|
- `projects`
|
||||||
|
- `thoughts`
|
||||||
|
- `stored_files`
|
||||||
|
- `learnings`
|
||||||
|
- `plans`
|
||||||
|
- `chat_histories`
|
||||||
|
- Add tenant indexes:
|
||||||
|
- single-column `tenant_key` for direct filtering
|
||||||
|
- `(tenant_key, name)` unique on `projects`, replacing global `projects.name` uniqueness
|
||||||
|
- `(tenant_key, 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_key` from context.
|
||||||
|
- Gets/updates/deletes by id or guid must add `and tenant_key = $n`.
|
||||||
|
- Lists/searches must add `tenant_key = $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_key` 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_key`. Otherwise leave `NULL` rows visible only to unauthenticated/internal single-tenant contexts.
|
||||||
|
3. Replace global project-name uniqueness with `(tenant_key, 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_key` 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_key` 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_key` 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_key` 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_key` 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
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bitechdev/ResolveSpec v1.1.15
|
git.warky.dev/wdevs/relspecgo v1.0.62
|
||||||
|
github.com/bitechdev/ResolveSpec v1.1.26
|
||||||
github.com/google/jsonschema-go v0.4.3
|
github.com/google/jsonschema-go v0.4.3
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.9.2
|
github.com/jackc/pgx/v5 v5.9.2
|
||||||
@@ -11,7 +12,7 @@ require (
|
|||||||
github.com/pgvector/pgvector-go v0.3.0
|
github.com/pgvector/pgvector-go v0.3.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/uptrace/bun v1.2.18
|
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/bun/driver/pgdriver v1.1.12
|
||||||
github.com/uptrace/bunrouter v1.0.23
|
github.com/uptrace/bunrouter v1.0.23
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.20.0
|
||||||
@@ -46,7 +47,6 @@ require (
|
|||||||
github.com/prometheus/procfs v0.20.1 // indirect
|
github.com/prometheus/procfs v0.20.1 // indirect
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.19.0 // 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/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
github.com/segmentio/asm v1.1.3 // indirect
|
github.com/segmentio/asm v1.1.3 // indirect
|
||||||
github.com/segmentio/encoding v0.5.4 // 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/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // 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/mssqldialect v1.2.18 // indirect
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 // indirect
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 // indirect
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // 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=
|
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 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
|
||||||
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
|
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.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.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=
|
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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
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.26 h1:/OYc1Mjcfm4Qxq8Xy5UY/32l5cSqkNe8ieeOqIEbK5o=
|
||||||
github.com/bitechdev/ResolveSpec v1.1.15/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs=
|
github.com/bitechdev/ResolveSpec v1.1.26/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 h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
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/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 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU=
|
||||||
github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y=
|
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.18 h1:nYzHoyJKJlIyl5i95Exi8ZTK8ooKWG+o3z3f404d/yQ=
|
||||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.16/go.mod h1:J5U7tGKWDsx2Q7MwDZF2417jCdpD6yD/ZMFJcCR80bk=
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.18/go.mod h1:Su45Je7z66sfeZ3d1ZsnOQEK8xfzGgaMzBvtoE8yFhk=
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4=
|
github.com/uptrace/bun/dialect/pgdialect v1.2.18 h1:IZ6nM2+OYrL8lkEAy7UkSEZvoa3vluTAUlZfPtlRB2k=
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
|
github.com/uptrace/bun/dialect/pgdialect v1.2.18/go.mod h1:Tqdf4QP1okrGYpXfodXvCOK6Ob1OOTwSaoAzCgBB3IU=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0=
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 h1:Z33SY/U++XK9uGWqS4h8OZVxfCXguIG+sU9cYq2PGFQ=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E=
|
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 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
|
||||||
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
|
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=
|
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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
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=
|
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 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
|||||||
+25
-20
@@ -194,26 +194,30 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
|||||||
adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger)
|
adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger)
|
||||||
|
|
||||||
toolSet := mcpserver.ToolSet{
|
toolSet := mcpserver.ToolSet{
|
||||||
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
|
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
|
||||||
Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
|
Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
|
||||||
List: tools.NewListTool(db, cfg.Search, activeProjects),
|
List: tools.NewListTool(db, cfg.Search, activeProjects),
|
||||||
Stats: tools.NewStatsTool(db),
|
Stats: tools.NewStatsTool(db),
|
||||||
Get: tools.NewGetTool(db),
|
Get: tools.NewGetTool(db),
|
||||||
Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
|
Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
|
||||||
Delete: tools.NewDeleteTool(db),
|
Delete: tools.NewDeleteTool(db),
|
||||||
Archive: tools.NewArchiveTool(db),
|
Archive: tools.NewArchiveTool(db),
|
||||||
Projects: tools.NewProjectsTool(db, activeProjects),
|
DuplicateAudit: tools.NewDuplicateAuditTool(db, cfg.Search, activeProjects),
|
||||||
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
Projects: tools.NewProjectsTool(db, activeProjects),
|
||||||
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
|
||||||
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
|
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
|
||||||
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
|
||||||
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
ProjectPersonas: tools.NewProjectPersonasTool(db, activeProjects),
|
||||||
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
WorldModel: tools.NewWorldModelTool(db, activeProjects),
|
||||||
Links: tools.NewLinksTool(db, embeddings, cfg.Search),
|
ThoughtLearningLinks: tools.NewThoughtLearningLinksTool(db),
|
||||||
Files: filesTool,
|
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
|
||||||
Backfill: backfillTool,
|
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
|
||||||
Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger),
|
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
|
||||||
RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer),
|
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),
|
//Maintenance: tools.NewMaintenanceTool(db),
|
||||||
Skills: tools.NewSkillsTool(db, activeProjects),
|
Skills: tools.NewSkillsTool(db, activeProjects),
|
||||||
Personas: tools.NewAgentPersonasTool(db),
|
Personas: tools.NewAgentPersonasTool(db),
|
||||||
@@ -235,6 +239,7 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
|
|||||||
}
|
}
|
||||||
mux.Handle("/files", authMiddleware(fileHandler(filesTool)))
|
mux.Handle("/files", authMiddleware(fileHandler(filesTool)))
|
||||||
mux.Handle("/files/{id}", 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("/.well-known/oauth-authorization-server", oauthMetadataHandler())
|
||||||
mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
|
mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
|
||||||
mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
|
mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ func resolveSpecModels() []resolveSpecModel {
|
|||||||
{schema: "public", entity: "plan_skills", model: generatedmodels.ModelPublicPlanSkills{}},
|
{schema: "public", entity: "plan_skills", model: generatedmodels.ModelPublicPlanSkills{}},
|
||||||
{schema: "public", entity: "plans", model: generatedmodels.ModelPublicPlans{}},
|
{schema: "public", entity: "plans", model: generatedmodels.ModelPublicPlans{}},
|
||||||
{schema: "public", entity: "project_guardrails", model: generatedmodels.ModelPublicProjectGuardrails{}},
|
{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: "project_skills", model: generatedmodels.ModelPublicProjectSkills{}},
|
||||||
{schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}},
|
{schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}},
|
||||||
{schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}},
|
{schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}},
|
||||||
|
{schema: "public", entity: "thought_learning_links", model: generatedmodels.ModelPublicThoughtLearningLinks{}},
|
||||||
{schema: "public", entity: "thought_links", model: generatedmodels.ModelPublicThoughtLinks{}},
|
{schema: "public", entity: "thought_links", model: generatedmodels.ModelPublicThoughtLinks{}},
|
||||||
{schema: "public", entity: "thoughts", model: generatedmodels.ModelPublicThoughts{}},
|
{schema: "public", entity: "thoughts", model: generatedmodels.ModelPublicThoughts{}},
|
||||||
{schema: "public", entity: "tool_annotations", model: generatedmodels.ModelPublicToolAnnotations{}},
|
{schema: "public", entity: "tool_annotations", model: generatedmodels.ModelPublicToolAnnotations{}},
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"git.warky.dev/wdevs/amcs/internal/config"
|
"git.warky.dev/wdevs/amcs/internal/config"
|
||||||
"git.warky.dev/wdevs/amcs/internal/observability"
|
"git.warky.dev/wdevs/amcs/internal/observability"
|
||||||
"git.warky.dev/wdevs/amcs/internal/requestip"
|
"git.warky.dev/wdevs/amcs/internal/requestip"
|
||||||
|
"git.warky.dev/wdevs/amcs/internal/tenancy"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey string
|
type contextKey string
|
||||||
@@ -50,6 +51,10 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
withTenant := func(ctx context.Context, keyID string) context.Context {
|
||||||
|
ctx = context.WithValue(ctx, keyIDContextKey, keyID)
|
||||||
|
return tenancy.WithTenantKey(ctx, keyID)
|
||||||
|
}
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
remoteAddr := requestip.FromRequest(r)
|
remoteAddr := requestip.FromRequest(r)
|
||||||
@@ -63,7 +68,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
recordAccess(r, keyID)
|
recordAccess(r, keyID)
|
||||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,14 +78,14 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
|||||||
if tokenStore != nil {
|
if tokenStore != nil {
|
||||||
if keyID, ok := tokenStore.Lookup(bearer); ok {
|
if keyID, ok := tokenStore.Lookup(bearer); ok {
|
||||||
recordAccess(r, keyID)
|
recordAccess(r, keyID)
|
||||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if keyring != nil {
|
if keyring != nil {
|
||||||
if keyID, ok := keyring.Lookup(bearer); ok {
|
if keyID, ok := keyring.Lookup(bearer); ok {
|
||||||
recordAccess(r, keyID)
|
recordAccess(r, keyID)
|
||||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +108,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
recordAccess(r, keyID)
|
recordAccess(r, keyID)
|
||||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +122,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
recordAccess(r, keyID)
|
recordAccess(r, keyID)
|
||||||
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
|
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,21 +3,21 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentGuardrails struct {
|
type ModelPublicAgentGuardrails struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_guardrails,alias:agent_guardrails"`
|
bun.BaseModel `bun:"table:public.agent_guardrails,alias:agent_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"`
|
||||||
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"`
|
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Severity resolvespec_common.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
|
Severity sql_types.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
|
||||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
RelGuardrailIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails
|
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
|
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
|
RelGuardrailIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
|
||||||
@@ -45,7 +45,7 @@ func (m ModelPublicAgentGuardrails) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentGuardrails) GetIDStr() string {
|
func (m ModelPublicAgentGuardrails) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,24 +3,24 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentParts struct {
|
type ModelPublicAgentParts struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_parts,alias:agent_parts"`
|
bun.BaseModel `bun:"table:public.agent_parts,alias:agent_parts"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
Content resolvespec_common.SqlString `bun:"content,type:text,default:'',notnull," json:"content"`
|
Content sql_types.SqlString `bun:"content,type:text,default:'',notnull," json:"content"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
PartType resolvespec_common.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
|
PartType sql_types.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt sql_types.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
|
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
|
RelPartIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicAgentParts
|
// TableName returns the table name for ModelPublicAgentParts
|
||||||
@@ -45,7 +45,7 @@ func (m ModelPublicAgentParts) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentParts) GetIDStr() string {
|
func (m ModelPublicAgentParts) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentPersonaGuardrails struct {
|
type ModelPublicAgentPersonaGuardrails struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_persona_guardrails,alias:agent_persona_guardrails"`
|
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"`
|
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull," json:"guardrail_id"`
|
||||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_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
|
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
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonaGuardrails) GetIDStr() string {
|
func (m ModelPublicAgentPersonaGuardrails) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentPersonaParts struct {
|
type ModelPublicAgentPersonaParts struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_persona_parts,alias:agent_persona_parts"`
|
bun.BaseModel `bun:"table:public.agent_persona_parts,alias:agent_persona_parts"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_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"`
|
PartOrder int32 `bun:"part_order,type:int,default:0,notnull," json:"part_order"`
|
||||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||||
Priority int32 `bun:"priority,type:int,default:0,notnull," json:"priority"`
|
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
|
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
|
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicAgentPersonaParts
|
// TableName returns the table name for ModelPublicAgentPersonaParts
|
||||||
@@ -40,7 +40,7 @@ func (m ModelPublicAgentPersonaParts) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonaParts) GetIDStr() string {
|
func (m ModelPublicAgentPersonaParts) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentPersonaSkills struct {
|
type ModelPublicAgentPersonaSkills struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_persona_skills,alias:agent_persona_skills"`
|
bun.BaseModel `bun:"table:public.agent_persona_skills,alias:agent_persona_skills"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
Override bool `bun:"override,type:boolean,default:false,notnull," json:"override"`
|
||||||
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
|
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||||
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
|
||||||
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
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
|
// TableName returns the table name for ModelPublicAgentPersonaSkills
|
||||||
@@ -38,7 +39,7 @@ func (m ModelPublicAgentPersonaSkills) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonaSkills) GetIDStr() string {
|
func (m ModelPublicAgentPersonaSkills) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentPersonaTraits struct {
|
type ModelPublicAgentPersonaTraits struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_persona_traits,alias:agent_persona_traits"`
|
bun.BaseModel `bun:"table:public.agent_persona_traits,alias:agent_persona_traits"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
|
||||||
TraitID int64 `bun:"trait_id,type:bigint,notnull," json:"trait_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
|
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
|
RelTraitID *ModelPublicAgentTraits `bun:"rel:has-one,join:trait_id=id" json:"reltraitid,omitempty"` // Has one ModelPublicAgentTraits
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicAgentPersonaTraits
|
// TableName returns the table name for ModelPublicAgentPersonaTraits
|
||||||
@@ -38,7 +38,7 @@ func (m ModelPublicAgentPersonaTraits) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonaTraits) GetIDStr() string {
|
func (m ModelPublicAgentPersonaTraits) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,26 +3,27 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentPersonas struct {
|
type ModelPublicAgentPersonas struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_personas,alias:agent_personas"`
|
bun.BaseModel `bun:"table:public.agent_personas,alias:agent_personas"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CompiledAt resolvespec_common.SqlTimeStamp `bun:"compiled_at,type:timestamptz,nullzero," json:"compiled_at"`
|
CompiledAt sql_types.SqlTimeStamp `bun:"compiled_at,type:timestamptz,nullzero," json:"compiled_at"`
|
||||||
CompiledDetail resolvespec_common.SqlString `bun:"compiled_detail,type:text,default:'',notnull," json:"compiled_detail"`
|
CompiledDetail sql_types.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"`
|
CompiledSummary sql_types.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"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
Detail resolvespec_common.SqlString `bun:"detail,type:text,default:'',notnull," json:"detail"`
|
Detail sql_types.SqlString `bun:"detail,type:text,default:'',notnull," json:"detail"`
|
||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
RelPersonaIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
|
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
|
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
|
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
|
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
|
RelPersonaIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||||
@@ -50,7 +51,7 @@ func (m ModelPublicAgentPersonas) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentPersonas) GetIDStr() string {
|
func (m ModelPublicAgentPersonas) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,28 +3,28 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentSkills struct {
|
type ModelPublicAgentSkills struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_skills,alias:agent_skills"`
|
bun.BaseModel `bun:"table:public.agent_skills,alias:agent_skills"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"`
|
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
DomainTags resolvespec_common.SqlStringArray `bun:"domain_tags,type:text,default:'{}',notnull," json:"domain_tags"`
|
DomainTags sql_types.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"`
|
FrameworkTags sql_types.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"`
|
GUID sql_types.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"`
|
LanguageTags sql_types.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"`
|
LibraryTags sql_types.SqlStringArray `bun:"library_tags,type:text[],default:'{}',notnull," json:"library_tags"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt sql_types.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
|
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
|
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
|
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
|
RelSkillIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicAgentSkills
|
// TableName returns the table name for ModelPublicAgentSkills
|
||||||
@@ -49,7 +49,7 @@ func (m ModelPublicAgentSkills) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentSkills) GetIDStr() string {
|
func (m ModelPublicAgentSkills) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicAgentTraits struct {
|
type ModelPublicAgentTraits struct {
|
||||||
bun.BaseModel `bun:"table:public.agent_traits,alias:agent_traits"`
|
bun.BaseModel `bun:"table:public.agent_traits,alias:agent_traits"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||||
Instruction resolvespec_common.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"`
|
Instruction sql_types.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
TraitType resolvespec_common.SqlString `bun:"trait_type,type:text,notnull," json:"trait_type"`
|
TraitType sql_types.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"`
|
UpdatedAt sql_types.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
|
RelTraitIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=trait_id" json:"reltraitidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicAgentTraits
|
// TableName returns the table name for ModelPublicAgentTraits
|
||||||
@@ -43,7 +43,7 @@ func (m ModelPublicAgentTraits) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicAgentTraits) GetIDStr() string {
|
func (m ModelPublicAgentTraits) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicArcStageParts struct {
|
type ModelPublicArcStageParts struct {
|
||||||
bun.BaseModel `bun:"table:public.arc_stage_parts,alias:arc_stage_parts"`
|
bun.BaseModel `bun:"table:public.arc_stage_parts,alias:arc_stage_parts"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_id"`
|
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_id"`
|
||||||
StageID int64 `bun:"stage_id,type:bigint,notnull," json:"stage_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
|
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
|
RelStageID *ModelPublicArcStages `bun:"rel:has-one,join:stage_id=id" json:"relstageid,omitempty"` // Has one ModelPublicArcStages
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicArcStageParts
|
// TableName returns the table name for ModelPublicArcStageParts
|
||||||
@@ -38,7 +38,7 @@ func (m ModelPublicArcStageParts) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicArcStageParts) GetIDStr() string {
|
func (m ModelPublicArcStageParts) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicArcStages struct {
|
type ModelPublicArcStages struct {
|
||||||
bun.BaseModel `bun:"table:public.arc_stages,alias:arc_stages"`
|
bun.BaseModel `bun:"table:public.arc_stages,alias:arc_stages"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
|
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
|
||||||
Condition resolvespec_common.SqlString `bun:"condition,type:text,default:'',notnull," json:"condition"`
|
Condition sql_types.SqlString `bun:"condition,type:text,default:'',notnull," json:"condition"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
StageOrder int32 `bun:"stage_order,type:int,default:0,notnull," json:"stage_order"`
|
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
|
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
|
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
|
RelCurrentStageIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=current_stage_id" json:"relcurrentstageidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicArcStages
|
// TableName returns the table name for ModelPublicArcStages
|
||||||
@@ -43,7 +43,7 @@ func (m ModelPublicArcStages) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicArcStages) GetIDStr() string {
|
func (m ModelPublicArcStages) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicCharacterArcs struct {
|
type ModelPublicCharacterArcs struct {
|
||||||
bun.BaseModel `bun:"table:public.character_arcs,alias:character_arcs"`
|
bun.BaseModel `bun:"table:public.character_arcs,alias:character_arcs"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,default:'',notnull," json:"summary"`
|
Summary sql_types.SqlString `bun:"summary,type:text,default:'',notnull," json:"summary"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
UpdatedAt sql_types.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
|
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
|
RelArcIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicCharacterArcs
|
// TableName returns the table name for ModelPublicCharacterArcs
|
||||||
@@ -41,7 +41,7 @@ func (m ModelPublicCharacterArcs) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicCharacterArcs) GetIDStr() string {
|
func (m ModelPublicCharacterArcs) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,25 +3,26 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicChatHistories struct {
|
type ModelPublicChatHistories struct {
|
||||||
bun.BaseModel `bun:"table:public.chat_histories,alias:chat_histories"`
|
bun.BaseModel `bun:"table:public.chat_histories,alias:chat_histories"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
AgentID resolvespec_common.SqlString `bun:"agent_id,type:text,nullzero," json:"agent_id"`
|
AgentID sql_types.SqlString `bun:"agent_id,type:text,nullzero," json:"agent_id"`
|
||||||
Channel resolvespec_common.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
|
Channel sql_types.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.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"`
|
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
||||||
Messages resolvespec_common.SqlJSONB `bun:"messages,type:jsonb,default:'',notnull," json:"messages"`
|
Messages sql_types.SqlJSONB `bun:"messages,type:jsonb,default:'[]',notnull," json:"messages"`
|
||||||
Metadata resolvespec_common.SqlJSONB `bun:"metadata,type:jsonb,default:'{}',notnull," json:"metadata"`
|
Metadata sql_types.SqlJSONB `bun:"metadata,type:jsonb,default:'{}',notnull," json:"metadata"`
|
||||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||||
SessionID resolvespec_common.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
|
SessionID sql_types.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,nullzero," json:"summary"`
|
Summary sql_types.SqlString `bun:"summary,type:text,nullzero," json:"summary"`
|
||||||
Title resolvespec_common.SqlString `bun:"title,type:text,nullzero," json:"title"`
|
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
Title sql_types.SqlString `bun:"title,type:text,nullzero," json:"title"`
|
||||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicChatHistories
|
// TableName returns the table name for ModelPublicChatHistories
|
||||||
@@ -46,7 +47,7 @@ func (m ModelPublicChatHistories) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicChatHistories) GetIDStr() string {
|
func (m ModelPublicChatHistories) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicEmbeddings struct {
|
type ModelPublicEmbeddings struct {
|
||||||
bun.BaseModel `bun:"table:public.embeddings,alias:embeddings"`
|
bun.BaseModel `bun:"table:public.embeddings,alias:embeddings"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||||
Dim int32 `bun:"dim,type:int,notnull," json:"dim"`
|
Dim int32 `bun:"dim,type:int,notnull," json:"dim"`
|
||||||
Embedding resolvespec_common.SqlVector `bun:"embedding,type:vector,notnull," json:"embedding"`
|
Embedding sql_types.SqlVector `bun:"embedding,type:vector,notnull," json:"embedding"`
|
||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
GUID sql_types.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"`
|
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"`
|
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"`
|
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
|
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicEmbeddings
|
// TableName returns the table name for ModelPublicEmbeddings
|
||||||
@@ -42,7 +42,7 @@ func (m ModelPublicEmbeddings) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicEmbeddings) GetIDStr() string {
|
func (m ModelPublicEmbeddings) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,39 +3,41 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicLearnings struct {
|
type ModelPublicLearnings struct {
|
||||||
bun.BaseModel `bun:"table:public.learnings,alias:learnings"`
|
bun.BaseModel `bun:"table:public.learnings,alias:learnings"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
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"`
|
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"`
|
Area sql_types.SqlString `bun:"area,type:text,default:'other',notnull," json:"area"`
|
||||||
Category resolvespec_common.SqlString `bun:"category,type:text,default:'insight',notnull," json:"category"`
|
Category sql_types.SqlString `bun:"category,type:text,default:'insight',notnull," json:"category"`
|
||||||
Confidence resolvespec_common.SqlString `bun:"confidence,type:text,default:'hypothesis',notnull," json:"confidence"`
|
Confidence sql_types.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"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Details resolvespec_common.SqlString `bun:"details,type:text,default:'',notnull," json:"details"`
|
Details sql_types.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"`
|
DuplicateOfLearningID sql_types.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"`
|
GUID sql_types.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"`
|
Priority sql_types.SqlString `bun:"priority,type:text,default:'medium',notnull," json:"priority"`
|
||||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
ProjectID sql_types.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"`
|
RelatedSkillID sql_types.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"`
|
RelatedThoughtID sql_types.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"`
|
ReviewedAt sql_types.SqlTimeStamp `bun:"reviewed_at,type:timestamptz,nullzero," json:"reviewed_at"`
|
||||||
ReviewedBy resolvespec_common.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
ReviewedBy sql_types.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
||||||
SourceRef resolvespec_common.SqlString `bun:"source_ref,type:text,nullzero," json:"source_ref"`
|
SourceRef sql_types.SqlString `bun:"source_ref,type:text,nullzero," json:"source_ref"`
|
||||||
SourceType resolvespec_common.SqlString `bun:"source_type,type:text,nullzero," json:"source_type"`
|
SourceType sql_types.SqlString `bun:"source_type,type:text,nullzero," json:"source_type"`
|
||||||
Status resolvespec_common.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
|
Status sql_types.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
|
||||||
Summary resolvespec_common.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
|
||||||
SupersedesLearningID resolvespec_common.SqlInt64 `bun:"supersedes_learning_id,type:bigint,nullzero," json:"supersedes_learning_id"`
|
SupersedesLearningID sql_types.SqlInt64 `bun:"supersedes_learning_id,type:bigint,nullzero," json:"supersedes_learning_id"`
|
||||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||||
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
|
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
|
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
|
||||||
RelRelatedSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:related_skill_id=id" json:"relrelatedskillid,omitempty"` // Has one ModelPublicAgentSkills
|
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||||
RelRelatedThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:related_thought_id=id" json:"relrelatedthoughtid,omitempty"` // Has one ModelPublicThoughts
|
RelRelatedSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:related_skill_id=id" json:"relrelatedskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||||
RelSupersedesLearningID *ModelPublicLearnings `bun:"rel:has-one,join:supersedes_learning_id=id" json:"relsupersedeslearningid,omitempty"` // Has one ModelPublicLearnings
|
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
|
||||||
|
RelLearningIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=learning_id" json:"rellearningidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicLearnings
|
// TableName returns the table name for ModelPublicLearnings
|
||||||
@@ -60,7 +62,7 @@ func (m ModelPublicLearnings) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicLearnings) GetIDStr() string {
|
func (m ModelPublicLearnings) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicOauthClients struct {
|
type ModelPublicOauthClients struct {
|
||||||
bun.BaseModel `bun:"table:public.oauth_clients,alias:oauth_clients"`
|
bun.BaseModel `bun:"table:public.oauth_clients,alias:oauth_clients"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
ClientID resolvespec_common.SqlString `bun:"client_id,type:text,notnull," json:"client_id"`
|
ClientID sql_types.SqlString `bun:"client_id,type:text,notnull," json:"client_id"`
|
||||||
ClientName resolvespec_common.SqlString `bun:"client_name,type:text,default:'',notnull," json:"client_name"`
|
ClientName sql_types.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"`
|
CreatedAt sql_types.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"`
|
RedirectUris sql_types.SqlStringArray `bun:"redirect_uris,type:text[],default:'{}',notnull," json:"redirect_uris"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicOauthClients
|
// TableName returns the table name for ModelPublicOauthClients
|
||||||
@@ -38,7 +38,7 @@ func (m ModelPublicOauthClients) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicOauthClients) GetIDStr() string {
|
func (m ModelPublicOauthClients) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicPersonaArc struct {
|
type ModelPublicPersonaArc struct {
|
||||||
bun.BaseModel `bun:"table:public.persona_arc,alias:persona_arc"`
|
bun.BaseModel `bun:"table:public.persona_arc,alias:persona_arc"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
PersonaID int64 `bun:"persona_id,type:bigint,pk," json:"persona_id"`
|
PersonaID int64 `bun:"persona_id,type:bigint,pk," json:"persona_id"`
|
||||||
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_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"`
|
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"`
|
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
|
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
|
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
|
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicPersonaArc
|
// TableName returns the table name for ModelPublicPersonaArc
|
||||||
@@ -41,7 +41,7 @@ func (m ModelPublicPersonaArc) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPersonaArc) GetIDStr() string {
|
func (m ModelPublicPersonaArc) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicPlanDependencies struct {
|
type ModelPublicPlanDependencies struct {
|
||||||
bun.BaseModel `bun:"table:public.plan_dependencies,alias:plan_dependencies"`
|
bun.BaseModel `bun:"table:public.plan_dependencies,alias:plan_dependencies"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
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"`
|
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"`
|
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
|
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
|
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicPlanDependencies
|
// TableName returns the table name for ModelPublicPlanDependencies
|
||||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanDependencies) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlanDependencies) GetIDStr() string {
|
func (m ModelPublicPlanDependencies) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicPlanGuardrails struct {
|
type ModelPublicPlanGuardrails struct {
|
||||||
bun.BaseModel `bun:"table:public.plan_guardrails,alias:plan_guardrails"`
|
bun.BaseModel `bun:"table:public.plan_guardrails,alias:plan_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"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
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"`
|
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"`
|
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
|
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
|
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicPlanGuardrails
|
// TableName returns the table name for ModelPublicPlanGuardrails
|
||||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanGuardrails) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlanGuardrails) GetIDStr() string {
|
func (m ModelPublicPlanGuardrails) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicPlanRelatedPlans struct {
|
type ModelPublicPlanRelatedPlans struct {
|
||||||
bun.BaseModel `bun:"table:public.plan_related_plans,alias:plan_related_plans"`
|
bun.BaseModel `bun:"table:public.plan_related_plans,alias:plan_related_plans"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
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"`
|
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"`
|
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
|
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
|
RelPlanBID *ModelPublicPlans `bun:"rel:has-one,join:plan_b_id=id" json:"relplanbid,omitempty"` // Has one ModelPublicPlans
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicPlanRelatedPlans
|
// TableName returns the table name for ModelPublicPlanRelatedPlans
|
||||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanRelatedPlans) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlanRelatedPlans) GetIDStr() string {
|
func (m ModelPublicPlanRelatedPlans) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicPlanSkills struct {
|
type ModelPublicPlanSkills struct {
|
||||||
bun.BaseModel `bun:"table:public.plan_skills,alias:plan_skills"`
|
bun.BaseModel `bun:"table:public.plan_skills,alias:plan_skills"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
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"`
|
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"`
|
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
|
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
|
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicPlanSkills
|
// TableName returns the table name for ModelPublicPlanSkills
|
||||||
@@ -39,7 +39,7 @@ func (m ModelPublicPlanSkills) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlanSkills) GetIDStr() string {
|
func (m ModelPublicPlanSkills) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,36 +3,37 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicPlans struct {
|
type ModelPublicPlans struct {
|
||||||
bun.BaseModel `bun:"table:public.plans,alias:plans"`
|
bun.BaseModel `bun:"table:public.plans,alias:plans"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CompletedAt resolvespec_common.SqlTimeStamp `bun:"completed_at,type:timestamptz,nullzero," json:"completed_at"`
|
CompletedAt sql_types.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"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
|
||||||
DueDate resolvespec_common.SqlTimeStamp `bun:"due_date,type:timestamptz,nullzero," json:"due_date"`
|
DueDate sql_types.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"`
|
GUID sql_types.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"`
|
LastReviewedAt sql_types.SqlTimeStamp `bun:"last_reviewed_at,type:timestamptz,nullzero," json:"last_reviewed_at"`
|
||||||
Owner resolvespec_common.SqlString `bun:"owner,type:text,nullzero," json:"owner"`
|
Owner sql_types.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
|
Priority sql_types.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"`
|
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||||
ReviewedBy resolvespec_common.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
|
ReviewedBy sql_types.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
|
Status sql_types.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"`
|
SupersedesPlanID sql_types.SqlInt64 `bun:"supersedes_plan_id,type:bigint,nullzero," json:"supersedes_plan_id"`
|
||||||
Tags resolvespec_common.SqlStringArray `bun:"tags,type:text,default:'{}',notnull," json:"tags"`
|
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
|
||||||
Title resolvespec_common.SqlString `bun:"title,type:text,notnull," json:"title"`
|
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
Title sql_types.SqlString `bun:"title,type:text,notnull," json:"title"`
|
||||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
RelSupersedesPlanID *ModelPublicPlans `bun:"rel:has-one,join:supersedes_plan_id=id" json:"relsupersedesplanid,omitempty"` // Has one ModelPublicPlans
|
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||||
RelDependsOnPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=depends_on_plan_id" json:"reldependsonplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
RelSupersedesPlanID *ModelPublicPlans `bun:"rel:has-one,join:supersedes_plan_id=id" json:"relsupersedesplanid,omitempty"` // Has one ModelPublicPlans
|
||||||
RelPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
RelDependsOnPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=depends_on_plan_id" json:"reldependsonplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||||
RelPlanAIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_a_id" json:"relplanaidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
RelPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
|
||||||
RelPlanBIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_b_id" json:"relplanbidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
RelPlanAIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_a_id" json:"relplanaidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||||
RelPlanIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
|
RelPlanBIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_b_id" json:"relplanbidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
|
||||||
RelPlanIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
|
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
|
// TableName returns the table name for ModelPublicPlans
|
||||||
@@ -57,7 +58,7 @@ func (m ModelPublicPlans) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicPlans) GetIDStr() string {
|
func (m ModelPublicPlans) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicProjectGuardrails struct {
|
type ModelPublicProjectGuardrails struct {
|
||||||
bun.BaseModel `bun:"table:public.project_guardrails,alias:project_guardrails"`
|
bun.BaseModel `bun:"table:public.project_guardrails,alias:project_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"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
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"`
|
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull," json:"guardrail_id"`
|
||||||
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_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
|
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
|
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicProjectGuardrails
|
// TableName returns the table name for ModelPublicProjectGuardrails
|
||||||
@@ -39,7 +39,7 @@ func (m ModelPublicProjectGuardrails) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicProjectGuardrails) GetIDStr() string {
|
func (m ModelPublicProjectGuardrails) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicProjectSkills struct {
|
type ModelPublicProjectSkills struct {
|
||||||
bun.BaseModel `bun:"table:public.project_skills,alias:project_skills"`
|
bun.BaseModel `bun:"table:public.project_skills,alias:project_skills"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
Override bool `bun:"override,type:boolean,default:false,notnull," json:"override"`
|
||||||
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
|
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
|
||||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
|
||||||
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
|
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
|
// TableName returns the table name for ModelPublicProjectSkills
|
||||||
@@ -39,7 +40,7 @@ func (m ModelPublicProjectSkills) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicProjectSkills) GetIDStr() string {
|
func (m ModelPublicProjectSkills) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,19 +3,20 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicProjects struct {
|
type ModelPublicProjects struct {
|
||||||
bun.BaseModel `bun:"table:public.projects,alias:projects"`
|
bun.BaseModel `bun:"table:public.projects,alias:projects"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
||||||
Description resolvespec_common.SqlString `bun:"description,type:text,nullzero," json:"description"`
|
Description sql_types.SqlString `bun:"description,type:text,nullzero," json:"description"`
|
||||||
GUID resolvespec_common.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
|
GUID sql_types.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"`
|
LastActiveAt sql_types.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"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull,unique:uidx_projects_tenant_key_name," json:"name"`
|
||||||
ThoughtCount resolvespec_common.SqlInt64 `bun:"thought_count,scanonly" json:"thought_count"`
|
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero,unique:uidx_projects_tenant_key_name," json:"tenant_key"`
|
||||||
|
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
|
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
|
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
|
RelProjectIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
|
||||||
@@ -47,7 +48,7 @@ func (m ModelPublicProjects) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicProjects) GetIDStr() string {
|
func (m ModelPublicProjects) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,27 +3,28 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicStoredFiles struct {
|
type ModelPublicStoredFiles struct {
|
||||||
bun.BaseModel `bun:"table:public.stored_files,alias:stored_files"`
|
bun.BaseModel `bun:"table:public.stored_files,alias:stored_files"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
Content []byte `bun:"content,type:bytea,notnull," json:"content"`
|
Content []byte `bun:"content,type:bytea,notnull," json:"content"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.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"`
|
Encoding sql_types.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"`
|
GUID sql_types.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"`
|
Kind sql_types.SqlString `bun:"kind,type:text,default:'file',notnull," json:"kind"`
|
||||||
MediaType resolvespec_common.SqlString `bun:"media_type,type:text,notnull," json:"media_type"`
|
MediaType sql_types.SqlString `bun:"media_type,type:text,notnull," json:"media_type"`
|
||||||
Name resolvespec_common.SqlString `bun:"name,type:text,notnull," json:"name"`
|
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
|
||||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
||||||
Sha256 resolvespec_common.SqlString `bun:"sha256,type:text,notnull," json:"sha256"`
|
Sha256 sql_types.SqlString `bun:"sha256,type:text,notnull," json:"sha256"`
|
||||||
SizeBytes int64 `bun:"size_bytes,type:bigint,notnull," json:"size_bytes"`
|
SizeBytes int64 `bun:"size_bytes,type:bigint,notnull," json:"size_bytes"`
|
||||||
ThoughtID resolvespec_common.SqlInt64 `bun:"thought_id,type:bigint,nullzero," json:"thought_id"`
|
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||||
UpdatedAt resolvespec_common.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
ThoughtID sql_types.SqlInt64 `bun:"thought_id,type:bigint,nullzero," json:"thought_id"`
|
||||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicStoredFiles
|
// TableName returns the table name for ModelPublicStoredFiles
|
||||||
@@ -48,7 +49,7 @@ func (m ModelPublicStoredFiles) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicStoredFiles) GetIDStr() string {
|
func (m ModelPublicStoredFiles) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// 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 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicThoughtLinks struct {
|
type ModelPublicThoughtLinks struct {
|
||||||
bun.BaseModel `bun:"table:public.thought_links,alias:thought_links"`
|
bun.BaseModel `bun:"table:public.thought_links,alias:thought_links"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
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"`
|
FromID int64 `bun:"from_id,type:bigint,notnull," json:"from_id"`
|
||||||
Relation resolvespec_common.SqlString `bun:"relation,type:text,notnull," json:"relation"`
|
Relation sql_types.SqlString `bun:"relation,type:text,notnull," json:"relation"`
|
||||||
ToID int64 `bun:"to_id,type:bigint,notnull," json:"to_id"`
|
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
|
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
|
RelToID *ModelPublicThoughts `bun:"rel:has-one,join:to_id=id" json:"reltoid,omitempty"` // Has one ModelPublicThoughts
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicThoughtLinks
|
// TableName returns the table name for ModelPublicThoughtLinks
|
||||||
@@ -40,7 +40,7 @@ func (m ModelPublicThoughtLinks) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicThoughtLinks) GetIDStr() string {
|
func (m ModelPublicThoughtLinks) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,26 +3,28 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicThoughts struct {
|
type ModelPublicThoughts struct {
|
||||||
bun.BaseModel `bun:"table:public.thoughts,alias:thoughts"`
|
bun.BaseModel `bun:"table:public.thoughts,alias:thoughts"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
ArchivedAt resolvespec_common.SqlTimeStamp `bun:"archived_at,type:timestamptz,nullzero," json:"archived_at"`
|
ArchivedAt sql_types.SqlTimeStamp `bun:"archived_at,type:timestamptz,nullzero," json:"archived_at"`
|
||||||
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"`
|
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
|
CreatedAt sql_types.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"`
|
GUID sql_types.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"`
|
Metadata sql_types.SqlJSONB `bun:"metadata,type:jsonb,default:{}::jsonb,nullzero," json:"metadata"`
|
||||||
ProjectID resolvespec_common.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
|
ProjectID sql_types.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"`
|
TenantKey sql_types.SqlString `bun:"tenant_key,type:text,nullzero," json:"tenant_key"`
|
||||||
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"`
|
||||||
RelFromIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=from_id" json:"relfromidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
|
||||||
RelToIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=to_id" json:"reltoidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
RelFromIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=from_id" json:"relfromidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
||||||
RelThoughtIDPublicEmbeddings []*ModelPublicEmbeddings `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicembeddings,omitempty"` // Has many ModelPublicEmbeddings
|
RelToIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=to_id" json:"reltoidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
|
||||||
RelThoughtIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
|
RelThoughtIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks
|
||||||
RelRelatedThoughtIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_thought_id" json:"relrelatedthoughtidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
|
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
|
// TableName returns the table name for ModelPublicThoughts
|
||||||
@@ -47,7 +49,7 @@ func (m ModelPublicThoughts) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicThoughts) GetIDStr() string {
|
func (m ModelPublicThoughts) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ package generatedmodels
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ModelPublicToolAnnotations struct {
|
type ModelPublicToolAnnotations struct {
|
||||||
bun.BaseModel `bun:"table:public.tool_annotations,alias:tool_annotations"`
|
bun.BaseModel `bun:"table:public.tool_annotations,alias:tool_annotations"`
|
||||||
ID resolvespec_common.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
|
||||||
CreatedAt resolvespec_common.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
|
||||||
Notes resolvespec_common.SqlString `bun:"notes,type:text,default:'',notnull," json:"notes"`
|
Notes sql_types.SqlString `bun:"notes,type:text,default:'',notnull," json:"notes"`
|
||||||
ToolName resolvespec_common.SqlString `bun:"tool_name,type:text,notnull," json:"tool_name"`
|
ToolName sql_types.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"`
|
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName returns the table name for ModelPublicToolAnnotations
|
// TableName returns the table name for ModelPublicToolAnnotations
|
||||||
@@ -38,7 +38,7 @@ func (m ModelPublicToolAnnotations) GetID() int64 {
|
|||||||
|
|
||||||
// GetIDStr returns the primary key as a string
|
// GetIDStr returns the primary key as a string
|
||||||
func (m ModelPublicToolAnnotations) GetIDStr() string {
|
func (m ModelPublicToolAnnotations) GetIDStr() string {
|
||||||
return fmt.Sprintf("%v", m.ID)
|
return m.ID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetID sets the primary key value
|
// SetID sets the primary key value
|
||||||
|
|||||||
+102
-24
@@ -1,6 +1,7 @@
|
|||||||
package mcpserver
|
package mcpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,31 +19,35 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ToolSet struct {
|
type ToolSet struct {
|
||||||
Version *tools.VersionTool
|
Version *tools.VersionTool
|
||||||
Capture *tools.CaptureTool
|
Capture *tools.CaptureTool
|
||||||
Search *tools.SearchTool
|
Search *tools.SearchTool
|
||||||
List *tools.ListTool
|
List *tools.ListTool
|
||||||
Stats *tools.StatsTool
|
Stats *tools.StatsTool
|
||||||
Get *tools.GetTool
|
Get *tools.GetTool
|
||||||
Update *tools.UpdateTool
|
Update *tools.UpdateTool
|
||||||
Delete *tools.DeleteTool
|
Delete *tools.DeleteTool
|
||||||
Archive *tools.ArchiveTool
|
Archive *tools.ArchiveTool
|
||||||
Projects *tools.ProjectsTool
|
DuplicateAudit *tools.DuplicateAuditTool
|
||||||
Context *tools.ContextTool
|
Projects *tools.ProjectsTool
|
||||||
Recall *tools.RecallTool
|
Context *tools.ContextTool
|
||||||
Summarize *tools.SummarizeTool
|
Recall *tools.RecallTool
|
||||||
Links *tools.LinksTool
|
Summarize *tools.SummarizeTool
|
||||||
Files *tools.FilesTool
|
Links *tools.LinksTool
|
||||||
Backfill *tools.BackfillTool
|
Files *tools.FilesTool
|
||||||
Reparse *tools.ReparseMetadataTool
|
Backfill *tools.BackfillTool
|
||||||
RetryMetadata *tools.RetryEnrichmentTool
|
Reparse *tools.ReparseMetadataTool
|
||||||
|
RetryMetadata *tools.RetryEnrichmentTool
|
||||||
//Maintenance *tools.MaintenanceTool
|
//Maintenance *tools.MaintenanceTool
|
||||||
Skills *tools.SkillsTool
|
Skills *tools.SkillsTool
|
||||||
Personas *tools.AgentPersonasTool
|
Personas *tools.AgentPersonasTool
|
||||||
ChatHistory *tools.ChatHistoryTool
|
ChatHistory *tools.ChatHistoryTool
|
||||||
Describe *tools.DescribeTool
|
Describe *tools.DescribeTool
|
||||||
Learnings *tools.LearningsTool
|
Learnings *tools.LearningsTool
|
||||||
Plans *tools.PlansTool
|
Plans *tools.PlansTool
|
||||||
|
ProjectPersonas *tools.ProjectPersonasTool
|
||||||
|
WorldModel *tools.WorldModelTool
|
||||||
|
ThoughtLearningLinks *tools.ThoughtLearningLinksTool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers groups the HTTP handlers produced for an MCP server instance.
|
// 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,
|
registerSystemTools,
|
||||||
registerThoughtTools,
|
registerThoughtTools,
|
||||||
registerProjectTools,
|
registerProjectTools,
|
||||||
|
registerWorldModelTools,
|
||||||
registerLearningTools,
|
registerLearningTools,
|
||||||
|
registerThoughtLearningLinkTools,
|
||||||
registerPlanTools,
|
registerPlanTools,
|
||||||
registerFileTools,
|
registerFileTools,
|
||||||
registerMaintenanceTools,
|
registerMaintenanceTools,
|
||||||
@@ -123,6 +130,33 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
|
|||||||
return h, nil
|
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.
|
// 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.
|
// Returns nil when publicURL is empty so the icons field is omitted from the MCP identity.
|
||||||
func buildServerIcons(publicURL string) []mcp.Icon {
|
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 {
|
}, toolSet.Archive.Handle); err != nil {
|
||||||
return err
|
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{
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
Name: "summarize_thoughts",
|
Name: "summarize_thoughts",
|
||||||
Description: "LLM summary of a filtered set of 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
|
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 {
|
func registerPlanTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
|
||||||
if err := addTool(server, logger, &mcp.Tool{
|
if err := addTool(server, logger, &mcp.Tool{
|
||||||
Name: "create_plan",
|
Name: "create_plan",
|
||||||
@@ -703,6 +771,7 @@ func BuildToolCatalog() []tools.ToolEntry {
|
|||||||
{Name: "update_thought", Description: "Update thought content or merge metadata.", Category: "thoughts"},
|
{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: "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: "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: "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: "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"},
|
{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: "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: "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: "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"},
|
{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
|
// learnings
|
||||||
{Name: "add_learning", Description: "Create a curated learning record distinct from raw thoughts.", Category: "projects"},
|
{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: "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: "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
|
// plans
|
||||||
{Name: "create_plan", Description: "Create a structured plan with status, priority, owner, due date, and optional project link.", Category: "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"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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 {
|
func newStreamableTestClient(t *testing.T) *mcp.ClientSession {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -189,28 +189,31 @@ func TestStreamableHTTPReturnsStructuredToolErrors(t *testing.T) {
|
|||||||
|
|
||||||
func streamableTestToolSet() ToolSet {
|
func streamableTestToolSet() ToolSet {
|
||||||
return 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"}),
|
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),
|
Capture: new(tools.CaptureTool),
|
||||||
Search: new(tools.SearchTool),
|
Search: new(tools.SearchTool),
|
||||||
List: new(tools.ListTool),
|
List: new(tools.ListTool),
|
||||||
Stats: new(tools.StatsTool),
|
Stats: new(tools.StatsTool),
|
||||||
Get: new(tools.GetTool),
|
Get: new(tools.GetTool),
|
||||||
Update: new(tools.UpdateTool),
|
Update: new(tools.UpdateTool),
|
||||||
Delete: new(tools.DeleteTool),
|
Delete: new(tools.DeleteTool),
|
||||||
Archive: new(tools.ArchiveTool),
|
Archive: new(tools.ArchiveTool),
|
||||||
Projects: new(tools.ProjectsTool),
|
Projects: new(tools.ProjectsTool),
|
||||||
Context: new(tools.ContextTool),
|
Context: new(tools.ContextTool),
|
||||||
Recall: new(tools.RecallTool),
|
Recall: new(tools.RecallTool),
|
||||||
Summarize: new(tools.SummarizeTool),
|
Summarize: new(tools.SummarizeTool),
|
||||||
Links: new(tools.LinksTool),
|
Links: new(tools.LinksTool),
|
||||||
Files: new(tools.FilesTool),
|
Files: new(tools.FilesTool),
|
||||||
Backfill: new(tools.BackfillTool),
|
Backfill: new(tools.BackfillTool),
|
||||||
Reparse: new(tools.ReparseMetadataTool),
|
Reparse: new(tools.ReparseMetadataTool),
|
||||||
RetryMetadata: new(tools.RetryEnrichmentTool),
|
RetryMetadata: new(tools.RetryEnrichmentTool),
|
||||||
Skills: new(tools.SkillsTool),
|
Skills: new(tools.SkillsTool),
|
||||||
ChatHistory: new(tools.ChatHistoryTool),
|
ChatHistory: new(tools.ChatHistoryTool),
|
||||||
Describe: new(tools.DescribeTool),
|
Describe: new(tools.DescribeTool),
|
||||||
Learnings: new(tools.LearningsTool),
|
Learnings: new(tools.LearningsTool),
|
||||||
Plans: new(tools.PlansTool),
|
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),
|
Type: normalizeType(in.Type),
|
||||||
Source: normalizeSource(in.Source),
|
Source: normalizeSource(in.Source),
|
||||||
Attachments: normalizeAttachments(in.Attachments),
|
Attachments: normalizeAttachments(in.Attachments),
|
||||||
|
Webhook: normalizeWebhook(in.Webhook),
|
||||||
MetadataStatus: normalizeMetadataStatus(in.MetadataStatus),
|
MetadataStatus: normalizeMetadataStatus(in.MetadataStatus),
|
||||||
MetadataUpdatedAt: strings.TrimSpace(in.MetadataUpdatedAt),
|
MetadataUpdatedAt: strings.TrimSpace(in.MetadataUpdatedAt),
|
||||||
MetadataLastAttemptedAt: strings.TrimSpace(in.MetadataLastAttemptedAt),
|
MetadataLastAttemptedAt: strings.TrimSpace(in.MetadataLastAttemptedAt),
|
||||||
@@ -201,10 +202,31 @@ func Merge(base, patch thoughttypes.ThoughtMetadata, capture config.CaptureConfi
|
|||||||
if len(patch.Attachments) > 0 {
|
if len(patch.Attachments) > 0 {
|
||||||
merged.Attachments = append(append([]thoughttypes.ThoughtAttachment{}, merged.Attachments...), patch.Attachments...)
|
merged.Attachments = append(append([]thoughttypes.ThoughtAttachment{}, merged.Attachments...), patch.Attachments...)
|
||||||
}
|
}
|
||||||
|
if patch.Webhook != nil {
|
||||||
|
merged.Webhook = patch.Webhook
|
||||||
|
}
|
||||||
|
|
||||||
return Normalize(merged, capture)
|
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 {
|
func normalizeAttachments(values []thoughttypes.ThoughtAttachment) []thoughttypes.ThoughtAttachment {
|
||||||
seen := make(map[string]struct{}, len(values))
|
seen := make(map[string]struct{}, len(values))
|
||||||
result := make([]thoughttypes.ThoughtAttachment, 0, 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,
|
Name: s.Name,
|
||||||
Description: s.Description,
|
Description: s.Description,
|
||||||
Tags: s.Tags,
|
Tags: s.Tags,
|
||||||
|
Override: s.Override,
|
||||||
}
|
}
|
||||||
if detail {
|
if detail {
|
||||||
entry.Content = s.Content
|
entry.Content = s.Content
|
||||||
@@ -341,6 +342,7 @@ func (db *DB) GetPersonaManifest(ctx context.Context, name string) (ext.PersonaM
|
|||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
Name: s.Name,
|
Name: s.Name,
|
||||||
Description: s.Description,
|
Description: s.Description,
|
||||||
|
Override: s.Override,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, g := range guardrails {
|
for _, g := range guardrails {
|
||||||
@@ -567,11 +569,12 @@ func (db *DB) RemovePersonaPart(ctx context.Context, personaName, partName strin
|
|||||||
// Persona-Skill links
|
// 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, `
|
_, err := db.pool.Exec(ctx, `
|
||||||
insert into agent_persona_skills (persona_id, skill_id)
|
insert into agent_persona_skills (persona_id, skill_id, override)
|
||||||
values ($1, $2) on conflict do nothing
|
values ($1, $2, $3)
|
||||||
`, personaID, skillID)
|
on conflict (persona_id, skill_id) do update set override = excluded.override
|
||||||
|
`, personaID, skillID, override)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add persona skill: %w", err)
|
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) {
|
func (db *DB) listPersonaSkills(ctx context.Context, personaID int64) ([]AgentSkillRow, error) {
|
||||||
rows, err := db.pool.Query(ctx, `
|
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
|
from agent_skills s
|
||||||
join agent_persona_skills aps on aps.skill_id = s.id
|
join agent_persona_skills aps on aps.skill_id = s.id
|
||||||
where aps.persona_id = $1
|
where aps.persona_id = $1
|
||||||
@@ -1073,7 +1076,7 @@ func (db *DB) listPersonaSkills(ctx context.Context, personaID int64) ([]AgentSk
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var s AgentSkillRow
|
var s AgentSkillRow
|
||||||
var tags []string
|
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)
|
return nil, fmt.Errorf("scan persona skill: %w", err)
|
||||||
}
|
}
|
||||||
s.Tags = nilToEmptyStrings(tags)
|
s.Tags = nilToEmptyStrings(tags)
|
||||||
@@ -1139,6 +1142,7 @@ type AgentSkillRow struct {
|
|||||||
Description string
|
Description string
|
||||||
Content string
|
Content string
|
||||||
Tags []string
|
Tags []string
|
||||||
|
Override bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentGuardrailRow struct {
|
type AgentGuardrailRow struct {
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
|
|
||||||
func (db *DB) InsertStoredFile(ctx context.Context, file thoughttypes.StoredFile) (thoughttypes.StoredFile, error) {
|
func (db *DB) InsertStoredFile(ctx context.Context, file thoughttypes.StoredFile) (thoughttypes.StoredFile, error) {
|
||||||
row := db.pool.QueryRow(ctx, `
|
row := db.pool.QueryRow(ctx, `
|
||||||
insert into stored_files (thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content)
|
insert into stored_files (thought_id, project_id, tenant_key, name, media_type, kind, encoding, size_bytes, sha256, content)
|
||||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
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
|
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
|
var model generatedmodels.ModelPublicStoredFiles
|
||||||
if err := row.Scan(
|
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) {
|
func (db *DB) GetStoredFile(ctx context.Context, id uuid.UUID) (thoughttypes.StoredFile, error) {
|
||||||
|
args := []any{id}
|
||||||
row := db.pool.QueryRow(ctx, `
|
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
|
select id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content, created_at, updated_at
|
||||||
from stored_files
|
from stored_files
|
||||||
where guid = $1
|
where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||||
`, id)
|
|
||||||
|
|
||||||
var model generatedmodels.ModelPublicStoredFiles
|
var model generatedmodels.ModelPublicStoredFiles
|
||||||
if err := row.Scan(
|
if err := row.Scan(
|
||||||
@@ -77,6 +77,7 @@ func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFil
|
|||||||
args := make([]any, 0, 4)
|
args := make([]any, 0, 4)
|
||||||
conditions := make([]string, 0, 3)
|
conditions := make([]string, 0, 3)
|
||||||
|
|
||||||
|
addTenantCondition(ctx, &args, &conditions, "tenant_key")
|
||||||
if filter.ThoughtID != nil {
|
if filter.ThoughtID != nil {
|
||||||
args = append(args, *filter.ThoughtID)
|
args = append(args, *filter.ThoughtID)
|
||||||
conditions = append(conditions, fmt.Sprintf("thought_id = $%d", len(args)))
|
conditions = append(conditions, fmt.Sprintf("thought_id = $%d", len(args)))
|
||||||
|
|||||||
@@ -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) {
|
func (db *DB) CreateProject(ctx context.Context, name, description string) (thoughttypes.Project, error) {
|
||||||
row := db.pool.QueryRow(ctx, `
|
row := db.pool.QueryRow(ctx, `
|
||||||
insert into projects (name, description)
|
insert into projects (name, description, tenant_key)
|
||||||
values ($1, $2)
|
values ($1, $2, $3)
|
||||||
returning id, guid, name, description, created_at, last_active_at
|
returning id, guid, name, description, created_at, last_active_at
|
||||||
`, name, description)
|
`, name, description, tenantKeyPtr(ctx))
|
||||||
|
|
||||||
var model generatedmodels.ModelPublicProjects
|
var model generatedmodels.ModelPublicProjects
|
||||||
if err := row.Scan(&model.ID, &model.GUID, &model.Name, &model.Description, &model.CreatedAt, &model.LastActiveAt); err != nil {
|
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) {
|
func (db *DB) getProjectByGUID(ctx context.Context, id uuid.UUID) (thoughttypes.Project, error) {
|
||||||
|
args := []any{id}
|
||||||
row := db.pool.QueryRow(ctx, `
|
row := db.pool.QueryRow(ctx, `
|
||||||
select id, guid, name, description, created_at, last_active_at
|
select id, guid, name, description, created_at, last_active_at
|
||||||
from projects
|
from projects
|
||||||
where guid = $1
|
where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||||
`, id)
|
|
||||||
return scanProject(row)
|
return scanProject(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) getProjectByName(ctx context.Context, name string) (thoughttypes.Project, error) {
|
func (db *DB) getProjectByName(ctx context.Context, name string) (thoughttypes.Project, error) {
|
||||||
|
args := []any{name}
|
||||||
row := db.pool.QueryRow(ctx, `
|
row := db.pool.QueryRow(ctx, `
|
||||||
select id, guid, name, description, created_at, last_active_at
|
select id, guid, name, description, created_at, last_active_at
|
||||||
from projects
|
from projects
|
||||||
where name = $1
|
where name = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||||
`, name)
|
|
||||||
return scanProject(row)
|
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) {
|
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_key = $1"
|
||||||
|
}
|
||||||
rows, err := db.pool.Query(ctx, `
|
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
|
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
|
from projects p
|
||||||
left join thoughts t on t.project_id = p.id and t.archived_at is null
|
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
|
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
|
order by p.last_active_at desc, p.created_at desc
|
||||||
`)
|
`, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list projects: %w", err)
|
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 {
|
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_key"), args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("touch project: %w", err)
|
return fmt.Errorf("touch project: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,24 @@ func scanSkill(row skillScanner) (ext.AgentSkill, error) {
|
|||||||
}, nil
|
}, 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) {
|
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)
|
row := db.pool.QueryRow(ctx, `select `+skillSelectCols+` from agent_skills where id = $1`, id)
|
||||||
s, err := scanSkill(row)
|
s, err := scanSkill(row)
|
||||||
@@ -278,12 +296,12 @@ func (db *DB) GetGuardrail(ctx context.Context, id int64) (ext.AgentGuardrail, e
|
|||||||
|
|
||||||
// Project Skills
|
// 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, `
|
_, err := db.pool.Exec(ctx, `
|
||||||
insert into project_skills (project_id, skill_id)
|
insert into project_skills (project_id, skill_id, override)
|
||||||
values ($1, $2)
|
values ($1, $2, $3)
|
||||||
on conflict do nothing
|
on conflict (project_id, skill_id) do update set override = excluded.override
|
||||||
`, projectID, skillID)
|
`, projectID, skillID, override)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add project skill: %w", err)
|
return fmt.Errorf("add project skill: %w", err)
|
||||||
}
|
}
|
||||||
@@ -305,7 +323,9 @@ func (db *DB) RemoveProjectSkill(ctx context.Context, projectID, skillID int64)
|
|||||||
|
|
||||||
func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.AgentSkill, error) {
|
func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.AgentSkill, error) {
|
||||||
rows, err := db.pool.Query(ctx, `
|
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
|
from agent_skills s
|
||||||
join project_skills ps on ps.skill_id = s.id
|
join project_skills ps on ps.skill_id = s.id
|
||||||
where ps.project_id = $1
|
where ps.project_id = $1
|
||||||
@@ -318,10 +338,13 @@ func (db *DB) ListProjectSkills(ctx context.Context, projectID int64) ([]ext.Age
|
|||||||
|
|
||||||
var skills []ext.AgentSkill
|
var skills []ext.AgentSkill
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
s, err := scanSkill(rows)
|
var s ext.AgentSkill
|
||||||
if err != nil {
|
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)
|
return nil, fmt.Errorf("scan project skill: %w", err)
|
||||||
}
|
}
|
||||||
|
normalizeSkillSlices(&s)
|
||||||
skills = append(skills, s)
|
skills = append(skills, s)
|
||||||
}
|
}
|
||||||
return skills, rows.Err()
|
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, `
|
row := tx.QueryRow(ctx, `
|
||||||
insert into thoughts (content, metadata, project_id)
|
insert into thoughts (content, metadata, project_id, tenant_key)
|
||||||
values ($1, $2::jsonb, $3)
|
values ($1, $2::jsonb, $3, $4)
|
||||||
returning id, guid, created_at, updated_at
|
returning id, guid, created_at, updated_at
|
||||||
`, thought.Content, metadata, thought.ProjectID)
|
`, thought.Content, metadata, thought.ProjectID, tenantKeyPtr(ctx))
|
||||||
|
|
||||||
created := thought
|
created := thought
|
||||||
created.Embedding = nil
|
created.Embedding = nil
|
||||||
@@ -68,6 +68,22 @@ func (db *DB) InsertThought(ctx context.Context, thought thoughttypes.Thought, e
|
|||||||
return created, nil
|
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) {
|
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)
|
filterJSON, err := json.Marshal(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,6 +123,7 @@ func (db *DB) ListThoughts(ctx context.Context, filter thoughttypes.ListFilter)
|
|||||||
args := make([]any, 0, 6)
|
args := make([]any, 0, 6)
|
||||||
conditions := []string{}
|
conditions := []string{}
|
||||||
|
|
||||||
|
addTenantCondition(ctx, &args, &conditions, "tenant_key")
|
||||||
if !filter.IncludeArchived {
|
if !filter.IncludeArchived {
|
||||||
conditions = append(conditions, "archived_at is null")
|
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) {
|
func (db *DB) Stats(ctx context.Context) (thoughttypes.ThoughtStats, error) {
|
||||||
var total int
|
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_key")
|
||||||
|
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)
|
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 {
|
if err != nil {
|
||||||
return thoughttypes.ThoughtStats{}, fmt.Errorf("query stats metadata: %w", err)
|
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) {
|
func (db *DB) GetThought(ctx context.Context, id uuid.UUID) (thoughttypes.Thought, error) {
|
||||||
|
args := []any{id}
|
||||||
row := db.pool.QueryRow(ctx, `
|
row := db.pool.QueryRow(ctx, `
|
||||||
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
||||||
from thoughts
|
from thoughts
|
||||||
where guid = $1
|
where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||||
`, id)
|
|
||||||
|
|
||||||
var model generatedmodels.ModelPublicThoughts
|
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 {
|
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) {
|
func (db *DB) GetThoughtByID(ctx context.Context, id int64) (thoughttypes.Thought, error) {
|
||||||
|
args := []any{id}
|
||||||
row := db.pool.QueryRow(ctx, `
|
row := db.pool.QueryRow(ctx, `
|
||||||
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
select id, guid, content, metadata, project_id, archived_at, created_at, updated_at
|
||||||
from thoughts
|
from thoughts
|
||||||
where id = $1
|
where id = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||||
`, id)
|
|
||||||
|
|
||||||
var model generatedmodels.ModelPublicThoughts
|
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 {
|
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)
|
_ = tx.Rollback(ctx)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
args := []any{id, content, metadataBytes, projectID}
|
||||||
tag, err := tx.Exec(ctx, `
|
tag, err := tx.Exec(ctx, `
|
||||||
update thoughts
|
update thoughts
|
||||||
set content = $2,
|
set content = $2,
|
||||||
metadata = $3::jsonb,
|
metadata = $3::jsonb,
|
||||||
project_id = $4,
|
project_id = $4,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
where guid = $1
|
where guid = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||||
`, id, content, metadataBytes, projectID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return thoughttypes.Thought{}, fmt.Errorf("update thought: %w", err)
|
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)
|
return thoughttypes.Thought{}, fmt.Errorf("marshal updated metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args := []any{id, metadataBytes}
|
||||||
tag, err := db.pool.Exec(ctx, `
|
tag, err := db.pool.Exec(ctx, `
|
||||||
update thoughts
|
update thoughts
|
||||||
set metadata = $2::jsonb,
|
set metadata = $2::jsonb,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
where id = $1
|
where id = $1`+tenantSQL(ctx, &args, "tenant_key"), args...)
|
||||||
`, id, metadataBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return thoughttypes.Thought{}, fmt.Errorf("update thought metadata: %w", err)
|
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 {
|
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_key"), args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delete thought: %w", err)
|
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 {
|
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_key"), args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archive thought: %w", err)
|
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",
|
"1 - (e.embedding <=> $1) > $2",
|
||||||
"e.model = $3",
|
"e.model = $3",
|
||||||
}
|
}
|
||||||
|
addTenantCondition(ctx, &args, &conditions, "t.tenant_key")
|
||||||
if projectID != nil {
|
if projectID != nil {
|
||||||
args = append(args, *projectID)
|
args = append(args, *projectID)
|
||||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
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",
|
"e.model = $1",
|
||||||
"t.archived_at is null",
|
"t.archived_at is null",
|
||||||
}
|
}
|
||||||
|
addTenantCondition(ctx, &args, &conditions, "t.tenant_key")
|
||||||
if projectID != nil {
|
if projectID != nil {
|
||||||
args = append(args, *projectID)
|
args = append(args, *projectID)
|
||||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
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) {
|
func (db *DB) ListThoughtsMissingEmbedding(ctx context.Context, model string, limit int, projectID *int64, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) {
|
||||||
args := []any{model}
|
args := []any{model}
|
||||||
conditions := []string{"e.id is null"}
|
conditions := []string{"e.id is null"}
|
||||||
|
addTenantCondition(ctx, &args, &conditions, "t.tenant_key")
|
||||||
|
|
||||||
if !includeArchived {
|
if !includeArchived {
|
||||||
conditions = append(conditions, "t.archived_at is null")
|
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) {
|
func (db *DB) ListThoughtsForMetadataReparse(ctx context.Context, limit int, projectID *int64, includeArchived bool, olderThanDays int) ([]thoughttypes.Thought, error) {
|
||||||
args := make([]any, 0, 3)
|
args := make([]any, 0, 3)
|
||||||
conditions := make([]string, 0, 4)
|
conditions := make([]string, 0, 4)
|
||||||
|
addTenantCondition(ctx, &args, &conditions, "tenant_key")
|
||||||
|
|
||||||
if !includeArchived {
|
if !includeArchived {
|
||||||
conditions = append(conditions, "archived_at is null")
|
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",
|
"t.archived_at is null",
|
||||||
"(to_tsvector('simple', t.content) || to_tsvector('simple', coalesce(p.name, ''))) @@ websearch_to_tsquery('simple', $1)",
|
"(to_tsvector('simple', t.content) || to_tsvector('simple', coalesce(p.name, ''))) @@ websearch_to_tsquery('simple', $1)",
|
||||||
}
|
}
|
||||||
|
addTenantCondition(ctx, &args, &conditions, "t.tenant_key")
|
||||||
if projectID != nil {
|
if projectID != nil {
|
||||||
args = append(args, *projectID)
|
args = append(args, *projectID)
|
||||||
conditions = append(conditions, fmt.Sprintf("t.project_id = $%d", len(args)))
|
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 {
|
type AddPersonaSkillInput struct {
|
||||||
PersonaID int64 `json:"persona_id" jsonschema:"persona id"`
|
PersonaID int64 `json:"persona_id" jsonschema:"persona id"`
|
||||||
SkillID int64 `json:"skill_id" jsonschema:"agent skill id to link"`
|
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 {
|
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) {
|
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{}, err
|
||||||
}
|
}
|
||||||
return nil, AddPersonaSkillOutput{PersonaID: in.PersonaID, SkillID: in.SkillID}, nil
|
return nil, AddPersonaSkillOutput{PersonaID: in.PersonaID, SkillID: in.SkillID}, nil
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ type ContextItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProjectContextOutput struct {
|
type ProjectContextOutput struct {
|
||||||
Project thoughttypes.Project `json:"project"`
|
Project thoughttypes.Project `json:"project"`
|
||||||
Context string `json:"context"`
|
Context string `json:"context"`
|
||||||
Items []ContextItem `json:"items"`
|
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 {
|
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)
|
query := strings.TrimSpace(in.Query)
|
||||||
if 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 {
|
if err != nil {
|
||||||
return nil, ProjectContextOutput{}, err
|
return nil, ProjectContextOutput{}, err
|
||||||
}
|
}
|
||||||
|
retrievalMode = mode
|
||||||
for _, result := range semantic {
|
for _, result := range semantic {
|
||||||
key := fmt.Sprint(result.ID)
|
key := fmt.Sprint(result.ID)
|
||||||
if _, ok := seen[key]; ok {
|
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)
|
_ = t.store.TouchProject(ctx, project.NumericID)
|
||||||
|
|
||||||
return nil, ProjectContextOutput{
|
return nil, ProjectContextOutput{
|
||||||
Project: *project,
|
Project: *project,
|
||||||
Context: contextBlock,
|
Context: contextBlock,
|
||||||
Items: items,
|
Items: items,
|
||||||
|
RetrievalMode: retrievalMode,
|
||||||
}, nil
|
}, 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 {
|
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 {
|
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
|
includeSemantic = *in.IncludeSemantic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var retrievalMode string
|
||||||
if includeSemantic {
|
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 {
|
if err != nil {
|
||||||
return nil, RelatedOutput{}, err
|
return nil, RelatedOutput{}, err
|
||||||
}
|
}
|
||||||
|
retrievalMode = mode
|
||||||
for _, item := range semantic {
|
for _, item := range semantic {
|
||||||
key := fmt.Sprint(item.ID)
|
key := fmt.Sprint(item.ID)
|
||||||
if _, ok := seen[key]; ok {
|
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 {
|
type RecallOutput struct {
|
||||||
Context string `json:"context"`
|
Context string `json:"context"`
|
||||||
Items []ContextItem `json:"items"`
|
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 {
|
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
|
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 {
|
if err != nil {
|
||||||
return nil, RecallOutput{}, err
|
return nil, RecallOutput{}, err
|
||||||
}
|
}
|
||||||
@@ -100,7 +101,8 @@ func (t *RecallTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil, RecallOutput{
|
return nil, RecallOutput{
|
||||||
Context: formatContextBlock(header, lines),
|
Context: formatContextBlock(header, lines),
|
||||||
Items: items,
|
Items: items,
|
||||||
|
RetrievalMode: mode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ import (
|
|||||||
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RetrievalModeSemantic = "semantic"
|
||||||
|
RetrievalModeText = "text"
|
||||||
|
)
|
||||||
|
|
||||||
// semanticSearch runs vector similarity search if embeddings exist for the
|
// semanticSearch runs vector similarity search if embeddings exist for the
|
||||||
// primary embedding model in the given scope, otherwise falls back to Postgres
|
// primary embedding model in the given scope, otherwise falls back to Postgres
|
||||||
// full-text search. Search always uses the primary model so query vectors
|
// full-text search. Search always uses the primary model so query vectors
|
||||||
// match rows stored under the primary model name.
|
// match rows stored under the primary model name.
|
||||||
|
// It returns the results and the retrieval mode used ("semantic" or "text").
|
||||||
func semanticSearch(
|
func semanticSearch(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
db *store.DB,
|
db *store.DB,
|
||||||
@@ -25,20 +31,22 @@ func semanticSearch(
|
|||||||
threshold float64,
|
threshold float64,
|
||||||
projectID *int64,
|
projectID *int64,
|
||||||
excludeID *uuid.UUID,
|
excludeID *uuid.UUID,
|
||||||
) ([]thoughttypes.SearchResult, error) {
|
) ([]thoughttypes.SearchResult, string, error) {
|
||||||
model := embeddings.PrimaryModel()
|
model := embeddings.PrimaryModel()
|
||||||
hasEmbeddings, err := db.HasEmbeddingsForModel(ctx, model, projectID)
|
hasEmbeddings, err := db.HasEmbeddingsForModel(ctx, model, projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasEmbeddings {
|
if hasEmbeddings {
|
||||||
embedding, err := embeddings.EmbedPrimary(ctx, query)
|
embedding, err := embeddings.EmbedPrimary(ctx, query)
|
||||||
if err != nil {
|
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 {
|
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 {
|
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)
|
_ = 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 {
|
if err != nil {
|
||||||
return nil, SearchOutput{}, err
|
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
|
// add_project_skill
|
||||||
|
|
||||||
type AddProjectSkillInput struct {
|
type AddProjectSkillInput struct {
|
||||||
Project string `json:"project,omitempty" jsonschema:"project name or id (uses active project if omitted)"`
|
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"`
|
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 {
|
type AddProjectSkillOutput struct {
|
||||||
@@ -273,7 +274,7 @@ func (t *SkillsTool) AddProjectSkill(ctx context.Context, req *mcp.CallToolReque
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, AddProjectSkillOutput{}, err
|
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{}, err
|
||||||
}
|
}
|
||||||
return nil, AddProjectSkillOutput{ProjectID: project.NumericID, SkillID: in.SkillID}, nil
|
return nil, AddProjectSkillOutput{ProjectID: project.NumericID, SkillID: in.SkillID}, nil
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ type SummarizeInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SummarizeOutput struct {
|
type SummarizeOutput struct {
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Count int `json:"count"`
|
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 {
|
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)
|
lines := make([]string, 0, limit)
|
||||||
count := 0
|
count := 0
|
||||||
|
|
||||||
|
var retrievalMode string
|
||||||
if query != "" {
|
if query != "" {
|
||||||
var projectID *int64
|
var projectID *int64
|
||||||
if project != nil {
|
if project != nil {
|
||||||
projectID = &project.NumericID
|
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 {
|
if err != nil {
|
||||||
return nil, SummarizeOutput{}, err
|
return nil, SummarizeOutput{}, err
|
||||||
}
|
}
|
||||||
|
retrievalMode = mode
|
||||||
for i, result := range results {
|
for i, result := range results {
|
||||||
lines = append(lines, thoughtContextLine(i, result.Content, result.Metadata, result.Similarity))
|
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)
|
_ = 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
|
// 2. active arc-stage parts
|
||||||
// 3. persona-linked parts (base)
|
// 3. persona-linked parts (base)
|
||||||
type PersonaFull struct {
|
type PersonaFull struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Body string `json:"body"` // summary or detail, depending on mode
|
Body string `json:"body"` // summary or detail, depending on mode
|
||||||
CompiledSummary string `json:"compiled_summary,omitempty"`
|
CompiledSummary string `json:"compiled_summary,omitempty"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Parts []AssembledPart `json:"parts"`
|
Parts []AssembledPart `json:"parts"`
|
||||||
Skills []PersonaSkillEntry `json:"skills"`
|
Skills []PersonaSkillEntry `json:"skills"`
|
||||||
Guardrails []PersonaGuardrailEntry `json:"guardrails"`
|
Guardrails []PersonaGuardrailEntry `json:"guardrails"`
|
||||||
Traits []PersonaTraitEntry `json:"traits"`
|
Traits []PersonaTraitEntry `json:"traits"`
|
||||||
Arc *PersonaArcState `json:"arc,omitempty"`
|
Arc *PersonaArcState `json:"arc,omitempty"`
|
||||||
Detail bool `json:"detail"` // whether full detail was requested
|
Detail bool `json:"detail"` // whether full detail was requested
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssembledPart struct {
|
type AssembledPart struct {
|
||||||
@@ -109,13 +109,14 @@ type PersonaSkillEntry struct {
|
|||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Content string `json:"content,omitempty"` // only when detail=true
|
Content string `json:"content,omitempty"` // only when detail=true
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
|
Override bool `json:"override,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersonaGuardrailEntry struct {
|
type PersonaGuardrailEntry struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description,omitempty"`
|
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
|
Severity string `json:"severity,omitempty"` // only when detail=true
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
@@ -168,6 +169,12 @@ type ManifestSkill struct {
|
|||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description,omitempty"`
|
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 {
|
type ManifestGuardrail struct {
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ type AgentSkill struct {
|
|||||||
DomainTags []string `json:"domain_tags"`
|
DomainTags []string `json:"domain_tags"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Override bool `json:"override,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentGuardrail struct {
|
type AgentGuardrail struct {
|
||||||
|
|||||||
@@ -39,3 +39,22 @@ type LinkedThought struct {
|
|||||||
Direction string `json:"direction"`
|
Direction string `json:"direction"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
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"`
|
Type string `json:"type"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Attachments []ThoughtAttachment `json:"attachments,omitempty"`
|
Attachments []ThoughtAttachment `json:"attachments,omitempty"`
|
||||||
|
Webhook *WebhookMetadata `json:"webhook,omitempty"`
|
||||||
MetadataStatus string `json:"metadata_status,omitempty"`
|
MetadataStatus string `json:"metadata_status,omitempty"`
|
||||||
MetadataUpdatedAt string `json:"metadata_updated_at,omitempty"`
|
MetadataUpdatedAt string `json:"metadata_updated_at,omitempty"`
|
||||||
MetadataLastAttemptedAt string `json:"metadata_last_attempted_at,omitempty"`
|
MetadataLastAttemptedAt string `json:"metadata_last_attempted_at,omitempty"`
|
||||||
MetadataError string `json:"metadata_error,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 {
|
type ThoughtAttachment struct {
|
||||||
FileID uuid.UUID `json:"file_id"`
|
FileID uuid.UUID `json:"file_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -30,19 +38,19 @@ type ThoughtAttachment struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StoredFile struct {
|
type StoredFile struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
GUID uuid.UUID `json:"guid"`
|
GUID uuid.UUID `json:"guid"`
|
||||||
ThoughtID *int64 `json:"thought_id,omitempty"`
|
ThoughtID *int64 `json:"thought_id,omitempty"`
|
||||||
ProjectID *int64 `json:"project_id,omitempty"`
|
ProjectID *int64 `json:"project_id,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
MediaType string `json:"media_type"`
|
MediaType string `json:"media_type"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
Encoding string `json:"encoding"`
|
Encoding string `json:"encoding"`
|
||||||
SizeBytes int64 `json:"size_bytes"`
|
SizeBytes int64 `json:"size_bytes"`
|
||||||
SHA256 string `json:"sha256"`
|
SHA256 string `json:"sha256"`
|
||||||
Content []byte `json:"-"`
|
Content []byte `json:"-"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoredFileFilter struct {
|
type StoredFileFilter struct {
|
||||||
|
|||||||
@@ -5,4 +5,7 @@ import _ "embed"
|
|||||||
var (
|
var (
|
||||||
//go:embed memory.md
|
//go:embed memory.md
|
||||||
MemoryInstructions []byte
|
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:
|
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
|
## Project Session Startup
|
||||||
|
|
||||||
@@ -51,7 +64,7 @@ Do not abandon the project scope or retry without a project. The project simply
|
|||||||
## Project Memory Rules
|
## Project Memory Rules
|
||||||
|
|
||||||
- Use project memory for code decisions, architecture, TODOs, debugging findings, and context specific to the current repo or workstream.
|
- 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.
|
- 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 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.
|
- 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
|
## 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
|
# 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)
|
## 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.
|
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.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
-- Many-to-many join table between thoughts and learnings.
|
||||||
|
-- Allows a learning to reference multiple source thoughts and a thought to
|
||||||
|
-- expose multiple related learnings, queryable in both directions.
|
||||||
|
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS public.identity_thought_learning_links_id
|
||||||
|
INCREMENT 1
|
||||||
|
MINVALUE 1
|
||||||
|
MAXVALUE 9223372036854775807
|
||||||
|
START 1
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.thought_learning_links (
|
||||||
|
id bigint NOT NULL DEFAULT nextval('public.identity_thought_learning_links_id'),
|
||||||
|
thought_id bigint NOT NULL REFERENCES public.thoughts(id) ON DELETE CASCADE,
|
||||||
|
learning_id bigint NOT NULL REFERENCES public.learnings(id) ON DELETE CASCADE,
|
||||||
|
relation text NOT NULL DEFAULT 'source',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT thought_learning_links_pkey PRIMARY KEY (id),
|
||||||
|
CONSTRAINT thought_learning_links_unique UNIQUE (thought_id, learning_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_thought_learning_links_thought_id
|
||||||
|
ON public.thought_learning_links (thought_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_thought_learning_links_learning_id
|
||||||
|
ON public.thought_learning_links (learning_id);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- Per-user tenancy: authenticate key/client id becomes an opaque tenant boundary.
|
||||||
|
-- Existing rows remain in the legacy unscoped tenant (NULL) for single-tenant installs.
|
||||||
|
|
||||||
|
alter table projects add column if not exists tenant_key text;
|
||||||
|
alter table thoughts add column if not exists tenant_key text;
|
||||||
|
alter table stored_files add column if not exists tenant_key text;
|
||||||
|
alter table learnings add column if not exists tenant_key text;
|
||||||
|
alter table plans add column if not exists tenant_key text;
|
||||||
|
alter table chat_histories add column if not exists tenant_key text;
|
||||||
|
|
||||||
|
-- Project names are now unique inside a tenant rather than globally.
|
||||||
|
alter table projects drop constraint if exists ukey_projects_name;
|
||||||
|
alter table projects drop constraint if exists projects_name_key;
|
||||||
|
drop index if exists projects_name_key;
|
||||||
|
create unique index if not exists projects_tenant_key_name_idx
|
||||||
|
on projects (coalesce(tenant_key, ''), name);
|
||||||
|
|
||||||
|
create index if not exists projects_tenant_key_idx on projects (tenant_key);
|
||||||
|
create index if not exists thoughts_tenant_key_idx on thoughts (tenant_key);
|
||||||
|
create index if not exists thoughts_tenant_key_project_id_idx on thoughts (tenant_key, project_id);
|
||||||
|
create index if not exists stored_files_tenant_key_idx on stored_files (tenant_key);
|
||||||
|
create index if not exists learnings_tenant_key_idx on learnings (tenant_key);
|
||||||
|
create index if not exists plans_tenant_key_idx on plans (tenant_key);
|
||||||
|
create index if not exists chat_histories_tenant_key_idx on chat_histories (tenant_key);
|
||||||
@@ -43,6 +43,7 @@ Table agent_persona_skills {
|
|||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
persona_id bigint [not null, ref: > agent_personas.id]
|
persona_id bigint [not null, ref: > agent_personas.id]
|
||||||
skill_id bigint [not null, ref: > agent_skills.id]
|
skill_id bigint [not null, ref: > agent_skills.id]
|
||||||
|
override boolean [not null, default: false]
|
||||||
|
|
||||||
indexes {
|
indexes {
|
||||||
(persona_id, skill_id) [uk]
|
(persona_id, skill_id) [uk]
|
||||||
@@ -50,6 +51,19 @@ Table agent_persona_skills {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Table project_personas {
|
||||||
|
id bigserial [pk]
|
||||||
|
project_id bigint [not null, ref: > projects.id]
|
||||||
|
persona_id bigint [not null, ref: > agent_personas.id]
|
||||||
|
is_default boolean [not null, default: false]
|
||||||
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(project_id, persona_id) [uk]
|
||||||
|
project_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Table agent_persona_guardrails {
|
Table agent_persona_guardrails {
|
||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
persona_id bigint [not null, ref: > agent_personas.id]
|
persona_id bigint [not null, ref: > agent_personas.id]
|
||||||
@@ -124,6 +138,8 @@ Ref: agent_persona_parts.persona_id > agent_personas.id [delete: cascade]
|
|||||||
Ref: agent_persona_parts.part_id > agent_parts.id [delete: cascade]
|
Ref: agent_persona_parts.part_id > agent_parts.id [delete: cascade]
|
||||||
Ref: agent_persona_skills.persona_id > agent_personas.id [delete: cascade]
|
Ref: agent_persona_skills.persona_id > agent_personas.id [delete: cascade]
|
||||||
Ref: agent_persona_skills.skill_id > agent_skills.id [delete: cascade]
|
Ref: agent_persona_skills.skill_id > agent_skills.id [delete: cascade]
|
||||||
|
Ref: project_personas.project_id > projects.id [delete: cascade]
|
||||||
|
Ref: project_personas.persona_id > agent_personas.id [delete: cascade]
|
||||||
Ref: agent_persona_guardrails.persona_id > agent_personas.id [delete: cascade]
|
Ref: agent_persona_guardrails.persona_id > agent_personas.id [delete: cascade]
|
||||||
Ref: agent_persona_guardrails.guardrail_id > agent_guardrails.id [delete: cascade]
|
Ref: agent_persona_guardrails.guardrail_id > agent_guardrails.id [delete: cascade]
|
||||||
Ref: agent_persona_traits.persona_id > agent_personas.id [delete: cascade]
|
Ref: agent_persona_traits.persona_id > agent_personas.id [delete: cascade]
|
||||||
|
|||||||
+27
-1
@@ -6,16 +6,28 @@ Table thoughts {
|
|||||||
created_at timestamptz [default: `now()`]
|
created_at timestamptz [default: `now()`]
|
||||||
updated_at timestamptz [default: `now()`]
|
updated_at timestamptz [default: `now()`]
|
||||||
project_id bigint [ref: > projects.id]
|
project_id bigint [ref: > projects.id]
|
||||||
|
tenant_key text
|
||||||
archived_at timestamptz
|
archived_at timestamptz
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
tenant_key
|
||||||
|
(tenant_key, project_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Table projects {
|
Table projects {
|
||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
||||||
name text [unique, not null]
|
name text [not null]
|
||||||
description text
|
description text
|
||||||
|
tenant_key text
|
||||||
created_at timestamptz [default: `now()`]
|
created_at timestamptz [default: `now()`]
|
||||||
last_active_at timestamptz [default: `now()`]
|
last_active_at timestamptz [default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(tenant_key, name) [unique]
|
||||||
|
tenant_key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Table thought_links {
|
Table thought_links {
|
||||||
@@ -32,6 +44,20 @@ Table thought_links {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Table thought_learning_links {
|
||||||
|
id bigserial [pk]
|
||||||
|
thought_id bigint [not null, ref: > thoughts.id]
|
||||||
|
learning_id bigint [not null, ref: > learnings.id]
|
||||||
|
relation text [not null, default: `'source'`]
|
||||||
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(thought_id, learning_id) [unique]
|
||||||
|
thought_id
|
||||||
|
learning_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Table embeddings {
|
Table embeddings {
|
||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Table stored_files {
|
|||||||
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
guid uuid [unique, not null, default: `gen_random_uuid()`]
|
||||||
thought_id bigint [ref: > thoughts.id]
|
thought_id bigint [ref: > thoughts.id]
|
||||||
project_id bigint [ref: > projects.id]
|
project_id bigint [ref: > projects.id]
|
||||||
|
tenant_key text
|
||||||
name text [not null]
|
name text [not null]
|
||||||
media_type text [not null]
|
media_type text [not null]
|
||||||
kind text [not null, default: 'file']
|
kind text [not null, default: 'file']
|
||||||
@@ -16,6 +17,7 @@ Table stored_files {
|
|||||||
indexes {
|
indexes {
|
||||||
thought_id
|
thought_id
|
||||||
project_id
|
project_id
|
||||||
|
tenant_key
|
||||||
sha256
|
sha256
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Table chat_histories {
|
|||||||
channel text
|
channel text
|
||||||
agent_id text
|
agent_id text
|
||||||
project_id bigint [ref: > projects.id]
|
project_id bigint [ref: > projects.id]
|
||||||
|
tenant_key text
|
||||||
messages jsonb [not null, default: `'[]'`]
|
messages jsonb [not null, default: `'[]'`]
|
||||||
summary text
|
summary text
|
||||||
metadata jsonb [not null, default: `'{}'`]
|
metadata jsonb [not null, default: `'{}'`]
|
||||||
@@ -15,6 +16,7 @@ Table chat_histories {
|
|||||||
indexes {
|
indexes {
|
||||||
session_id
|
session_id
|
||||||
project_id
|
project_id
|
||||||
|
tenant_key
|
||||||
channel
|
channel
|
||||||
agent_id
|
agent_id
|
||||||
created_at
|
created_at
|
||||||
@@ -46,6 +48,7 @@ Table learnings {
|
|||||||
source_type text
|
source_type text
|
||||||
source_ref text
|
source_ref text
|
||||||
project_id bigint [ref: > projects.id]
|
project_id bigint [ref: > projects.id]
|
||||||
|
tenant_key text
|
||||||
related_thought_id bigint [ref: > thoughts.id]
|
related_thought_id bigint [ref: > thoughts.id]
|
||||||
related_skill_id bigint [ref: > agent_skills.id]
|
related_skill_id bigint [ref: > agent_skills.id]
|
||||||
reviewed_by text
|
reviewed_by text
|
||||||
@@ -58,6 +61,7 @@ Table learnings {
|
|||||||
|
|
||||||
indexes {
|
indexes {
|
||||||
project_id
|
project_id
|
||||||
|
tenant_key
|
||||||
category
|
category
|
||||||
area
|
area
|
||||||
status
|
status
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Table plans {
|
|||||||
status text [not null, default: 'draft'] // draft, active, blocked, completed, cancelled, superseded
|
status text [not null, default: 'draft'] // draft, active, blocked, completed, cancelled, superseded
|
||||||
priority text [not null, default: 'medium'] // low, medium, high, critical
|
priority text [not null, default: 'medium'] // low, medium, high, critical
|
||||||
project_id bigint [ref: > projects.id]
|
project_id bigint [ref: > projects.id]
|
||||||
|
tenant_key text
|
||||||
owner text
|
owner text
|
||||||
due_date timestamptz
|
due_date timestamptz
|
||||||
completed_at timestamptz
|
completed_at timestamptz
|
||||||
@@ -18,6 +19,7 @@ Table plans {
|
|||||||
|
|
||||||
indexes {
|
indexes {
|
||||||
project_id
|
project_id
|
||||||
|
tenant_key
|
||||||
status
|
status
|
||||||
priority
|
priority
|
||||||
owner
|
owner
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Table project_skills {
|
|||||||
id bigserial [pk]
|
id bigserial [pk]
|
||||||
project_id bigint [not null, ref: > projects.id]
|
project_id bigint [not null, ref: > projects.id]
|
||||||
skill_id bigint [not null, ref: > agent_skills.id]
|
skill_id bigint [not null, ref: > agent_skills.id]
|
||||||
|
override boolean [not null, default: false]
|
||||||
created_at timestamptz [not null, default: `now()`]
|
created_at timestamptz [not null, default: `now()`]
|
||||||
|
|
||||||
indexes {
|
indexes {
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ mkdir -p "${out_dir}"
|
|||||||
--from-list "${schema_list}" \
|
--from-list "${schema_list}" \
|
||||||
--to bun \
|
--to bun \
|
||||||
--to-path "${out_dir}" \
|
--to-path "${out_dir}" \
|
||||||
--package generatedmodels
|
--package generatedmodels \
|
||||||
|
--types resolvespec
|
||||||
|
|
||||||
"${relspec_bin}" templ \
|
"${relspec_bin}" templ \
|
||||||
--from dbml \
|
--from dbml \
|
||||||
@@ -52,6 +53,7 @@ mkdir -p "${out_dir}"
|
|||||||
--template "${resolve_spec_template}" \
|
--template "${resolve_spec_template}" \
|
||||||
--output "${resolve_spec_models_file}"
|
--output "${resolve_spec_models_file}"
|
||||||
|
|
||||||
|
|
||||||
# relspec currently emits a few files with unused fmt imports; strip only when fmt is unused.
|
# relspec currently emits a few files with unused fmt imports; strip only when fmt is unused.
|
||||||
for file in "${out_dir}"/*.go; do
|
for file in "${out_dir}"/*.go; do
|
||||||
sed -i 's/fmt.Sprintf("%d", m.ID)/fmt.Sprintf("%v", m.ID)/g' "${file}"
|
sed -i 's/fmt.Sprintf("%d", m.ID)/fmt.Sprintf("%v", m.ID)/g' "${file}"
|
||||||
|
|||||||
+2
-1
@@ -143,7 +143,8 @@
|
|||||||
unique_tools: raw?.metrics?.unique_tools ?? 0,
|
unique_tools: raw?.metrics?.unique_tools ?? 0,
|
||||||
top_ips: Array.isArray(raw?.metrics?.top_ips) ? raw.metrics.top_ips : [],
|
top_ips: Array.isArray(raw?.metrics?.top_ips) ? raw.metrics.top_ips : [],
|
||||||
top_agents: Array.isArray(raw?.metrics?.top_agents) ? raw.metrics.top_agents : [],
|
top_agents: Array.isArray(raw?.metrics?.top_agents) ? raw.metrics.top_agents : [],
|
||||||
top_tools: Array.isArray(raw?.metrics?.top_tools) ? raw.metrics.top_tools : []
|
top_tools: Array.isArray(raw?.metrics?.top_tools) ? raw.metrics.top_tools : [],
|
||||||
|
recent_log: Array.isArray(raw?.metrics?.recent_log) ? raw.metrics.recent_log : []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -98,12 +98,6 @@
|
|||||||
activeCard = id;
|
activeCard = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCardKeydown(event: KeyboardEvent, id: string) {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
setActiveCard(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -205,17 +199,15 @@
|
|||||||
|
|
||||||
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{#each intelligenceCards as card}
|
{#each intelligenceCards as card}
|
||||||
<article
|
<button
|
||||||
class={`rounded-xl border p-4 transition ${
|
type="button"
|
||||||
|
class={`w-full rounded-xl border p-4 text-left transition ${
|
||||||
activeCard === card.id
|
activeCard === card.id
|
||||||
? "border-cyan-300/40 bg-cyan-400/[0.08] shadow-lg shadow-cyan-950/30"
|
? "border-cyan-300/40 bg-cyan-400/[0.08] shadow-lg shadow-cyan-950/30"
|
||||||
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]"
|
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]"
|
||||||
}`}
|
}`}
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-pressed={activeCard === card.id}
|
aria-pressed={activeCard === card.id}
|
||||||
onclick={() => setActiveCard(card.id)}
|
onclick={() => setActiveCard(card.id)}
|
||||||
onkeydown={(event) => handleCardKeydown(event, card.id)}
|
|
||||||
>
|
>
|
||||||
<p class={`text-xs uppercase tracking-[0.16em] ${card.accentClass}`}>
|
<p class={`text-xs uppercase tracking-[0.16em] ${card.accentClass}`}>
|
||||||
{card.title}
|
{card.title}
|
||||||
@@ -233,7 +225,7 @@
|
|||||||
: ""}
|
: ""}
|
||||||
{/each}
|
{/each}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2025 wdevs
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# sqltypes
|
||||||
|
|
||||||
|
Nullable SQL types for hand-written or generated Go models. Each type wraps a
|
||||||
|
value with a `Valid` flag and implements `database/sql.Scanner`,
|
||||||
|
`driver.Valuer`, `encoding/json`, `gopkg.in/yaml.v3`, and `encoding/xml`
|
||||||
|
marshalling — so a single struct field can be scanned from a database row,
|
||||||
|
round-tripped through JSON/YAML/XML, and written back to the database without
|
||||||
|
any per-format glue code.
|
||||||
|
|
||||||
|
This package is what the `bun` and `gorm` writers emit when generating models
|
||||||
|
with `--types sqltypes` (see [`pkg/writers/bun`](../writers/bun/README.md) and
|
||||||
|
[`pkg/writers/gorm`](../writers/gorm/README.md)). It can also be imported
|
||||||
|
directly in hand-written models.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```go
|
||||||
|
import sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scalar types
|
||||||
|
|
||||||
|
All scalar types are instantiations of the generic `SqlNull[T]`:
|
||||||
|
|
||||||
|
| Type | Underlying | Typical SQL type |
|
||||||
|
|---|---|---|
|
||||||
|
| `SqlInt16` | `int16` | `smallint` |
|
||||||
|
| `SqlInt32` | `int32` | `integer` |
|
||||||
|
| `SqlInt64` | `int64` | `bigint` |
|
||||||
|
| `SqlFloat32` | `float32` | `real`, `float4` |
|
||||||
|
| `SqlFloat64` | `float64` | `double precision`, `numeric`, `decimal`, `money` |
|
||||||
|
| `SqlBool` | `bool` | `boolean` |
|
||||||
|
| `SqlString` | `string` | `text`, `varchar`, `char`, `citext`, `inet`, `cidr`, `macaddr` |
|
||||||
|
| `SqlByteArray` | `[]byte` | `bytea` (base64-encoded in JSON/YAML/XML) |
|
||||||
|
| `SqlUUID` | `uuid.UUID` (`github.com/google/uuid`) | `uuid` |
|
||||||
|
|
||||||
|
You can also instantiate `SqlNull[T]` directly for any type not covered
|
||||||
|
above, e.g. `SqlNull[MyEnum]`.
|
||||||
|
|
||||||
|
### Date/time types
|
||||||
|
|
||||||
|
Plain `time.Time` doesn't distinguish date-only, time-only, and timestamp
|
||||||
|
semantics, and its zero value marshals to a confusing `0001-01-01T00:00:00Z`.
|
||||||
|
These wrapper types fix both problems:
|
||||||
|
|
||||||
|
| Type | Format | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `SqlTimeStamp` | `2006-01-02T15:04:05` | Full timestamp |
|
||||||
|
| `SqlDate` | `2006-01-02` | Date only |
|
||||||
|
| `SqlTime` | `15:04:05` | Time only |
|
||||||
|
|
||||||
|
Zero/pre-epoch values (`time.Time{}` or anything before `0002-01-01`) marshal
|
||||||
|
to `null` and `Value()` returns `nil`, instead of leaking Go's zero-time
|
||||||
|
sentinel into the database or API responses.
|
||||||
|
|
||||||
|
### JSON types
|
||||||
|
|
||||||
|
| Type | Underlying | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `SqlJSONB` | `[]byte` | Raw JSON bytes; `MarshalYAML` decodes to native YAML mappings/sequences instead of an embedded JSON string |
|
||||||
|
| `SqlJSON` | `= SqlJSONB` | Alias — PostgreSQL's `json` and `jsonb` share the same Go representation |
|
||||||
|
|
||||||
|
`SqlJSONB` has `AsMap()` / `AsSlice()` helpers for pulling out
|
||||||
|
`map[string]any` / `[]any` without a separate `json.Unmarshal` call.
|
||||||
|
|
||||||
|
### Vector type (pgvector)
|
||||||
|
|
||||||
|
`SqlVector` wraps `[]float32` for the `vector` column type ([pgvector](https://github.com/pgvector/pgvector)),
|
||||||
|
scanning/writing the `[1,2,3]` literal format pgvector uses over the wire.
|
||||||
|
|
||||||
|
## Array types
|
||||||
|
|
||||||
|
PostgreSQL array columns (`text[]`, `integer[]`, …) map to `SqlXxxArray`
|
||||||
|
types, each wrapping `Val []T` + `Valid bool` and handling PostgreSQL's
|
||||||
|
`{a,b,c}` array literal format on `Scan`/`Value`:
|
||||||
|
|
||||||
|
`SqlStringArray`, `SqlInt16Array`, `SqlInt32Array`, `SqlInt64Array`,
|
||||||
|
`SqlFloat32Array`, `SqlFloat64Array`, `SqlBoolArray`, `SqlUUIDArray`.
|
||||||
|
|
||||||
|
## Constructing values
|
||||||
|
|
||||||
|
Every type has a `NewSqlXxx(v)` constructor that sets `Valid: true`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
name := sql_types.NewSqlString("Ada Lovelace")
|
||||||
|
age := sql_types.NewSqlInt32(36)
|
||||||
|
tags := sql_types.NewSqlStringArray([]string{"engineer", "mathematician"})
|
||||||
|
```
|
||||||
|
|
||||||
|
The zero value of any type (`sql_types.SqlString{}`) is null/invalid — use it
|
||||||
|
directly for a `NULL` field instead of a separate constructor.
|
||||||
|
|
||||||
|
Generic helpers:
|
||||||
|
|
||||||
|
```go
|
||||||
|
sql_types.Null(v, valid) // SqlNull[T]{Val: v, Valid: valid}
|
||||||
|
sql_types.NewSql[T](anyValue) // best-effort conversion from any Go value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reading values back
|
||||||
|
|
||||||
|
Each scalar type has typed accessors that return the zero value instead of
|
||||||
|
panicking when `Valid` is false:
|
||||||
|
|
||||||
|
```go
|
||||||
|
n.Int64() // SqlInt16/32/64, SqlFloat32/64, SqlBool, SqlString → int64
|
||||||
|
n.Float64() // → float64
|
||||||
|
n.Bool() // → bool
|
||||||
|
n.Time() // SqlNull[time.Time]-based types → time.Time
|
||||||
|
n.UUID() // SqlUUID → uuid.UUID
|
||||||
|
n.String() // fmt.Stringer — empty string when invalid
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID sql_types.SqlUUID `json:"id"`
|
||||||
|
Name sql_types.SqlString `json:"name"`
|
||||||
|
Tags sql_types.SqlStringArray `json:"tags"`
|
||||||
|
Metadata sql_types.SqlJSONB `json:"metadata"`
|
||||||
|
CreatedAt sql_types.SqlTimeStamp `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
u := User{
|
||||||
|
ID: sql_types.NewSqlUUID(uuid.New()),
|
||||||
|
Name: sql_types.NewSqlString("Ada Lovelace"),
|
||||||
|
Tags: sql_types.NewSqlStringArray([]string{"engineer"}),
|
||||||
|
CreatedAt: sql_types.SqlTimeStampNow(),
|
||||||
|
}
|
||||||
|
// Metadata left as the zero value → serializes as null, scans as NULL.
|
||||||
|
```
|
||||||
|
|
||||||
|
Every type implements `sql.Scanner` and `driver.Valuer`, so these fields can
|
||||||
|
be used directly as struct fields with `database/sql`, `bun`, or `gorm`
|
||||||
|
without additional tags or hooks.
|
||||||
+305
-1
@@ -1,15 +1,85 @@
|
|||||||
package spectypes
|
package sqltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// marshalYAMLSlice returns the value used by yaml.Marshaler implementations
|
||||||
|
// for the nullable array types below: nil when invalid, the slice otherwise.
|
||||||
|
func marshalYAMLSlice[T any](valid bool, vals []T) (any, error) {
|
||||||
|
if !valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalYAMLSlice returns the value used by yaml.Unmarshaler
|
||||||
|
// implementations for the nullable array types below.
|
||||||
|
func unmarshalYAMLSlice[T any](value *yaml.Node) (vals []T, valid bool, err error) {
|
||||||
|
if value == nil || value.Tag == "!!null" {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
if err := value.Decode(&vals); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return vals, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// xmlArrayItemName is the element name used for each item when a nullable
|
||||||
|
// array type is encoded as XML, since XML has no native list type.
|
||||||
|
var xmlArrayItemName = xml.Name{Local: "item"}
|
||||||
|
|
||||||
|
// marshalXMLSlice writes a nullable array type as XML: an empty element when
|
||||||
|
// invalid, otherwise the start tag followed by one <item> child per element.
|
||||||
|
func marshalXMLSlice[T any](e *xml.Encoder, start xml.StartElement, valid bool, vals []T) error {
|
||||||
|
if !valid {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, v := range vals {
|
||||||
|
if err := e.EncodeElement(v, xml.StartElement{Name: xmlArrayItemName}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.EncodeToken(start.End())
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalXMLSlice reads back the XML written by marshalXMLSlice.
|
||||||
|
//
|
||||||
|
// XML has no native null representation: an element with no <item> children
|
||||||
|
// unmarshals to a valid, empty slice rather than an invalid one.
|
||||||
|
func unmarshalXMLSlice[T any](d *xml.Decoder, start xml.StartElement) ([]T, error) {
|
||||||
|
var vals []T
|
||||||
|
for {
|
||||||
|
tok, err := d.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch t := tok.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
var v T
|
||||||
|
if err := d.DecodeElement(&v, &t); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
vals = append(vals, v)
|
||||||
|
case xml.EndElement:
|
||||||
|
if t.Name == start.Name {
|
||||||
|
return vals, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parsePostgresArrayElements parses a PostgreSQL array literal (e.g. `{a,"b,c",d}`)
|
// parsePostgresArrayElements parses a PostgreSQL array literal (e.g. `{a,"b,c",d}`)
|
||||||
// into a slice of raw string elements. Each element retains its unquoted/unescaped value.
|
// into a slice of raw string elements. Each element retains its unquoted/unescaped value.
|
||||||
func parsePostgresArrayElements(s string) ([]string, error) {
|
func parsePostgresArrayElements(s string) ([]string, error) {
|
||||||
@@ -140,6 +210,32 @@ func (a *SqlStringArray) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlStringArray) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlStringArray) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[string](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlStringArray) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlStringArray) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[string](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlStringArray(v []string) SqlStringArray {
|
func NewSqlStringArray(v []string) SqlStringArray {
|
||||||
return SqlStringArray{Val: v, Valid: true}
|
return SqlStringArray{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -215,6 +311,32 @@ func (a *SqlInt16Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlInt16Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt16Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[int16](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt16Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt16Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[int16](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlInt16Array(v []int16) SqlInt16Array {
|
func NewSqlInt16Array(v []int16) SqlInt16Array {
|
||||||
return SqlInt16Array{Val: v, Valid: true}
|
return SqlInt16Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -290,6 +412,32 @@ func (a *SqlInt32Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlInt32Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt32Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[int32](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt32Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt32Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[int32](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlInt32Array(v []int32) SqlInt32Array {
|
func NewSqlInt32Array(v []int32) SqlInt32Array {
|
||||||
return SqlInt32Array{Val: v, Valid: true}
|
return SqlInt32Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -365,6 +513,32 @@ func (a *SqlInt64Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlInt64Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt64Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[int64](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlInt64Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlInt64Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[int64](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlInt64Array(v []int64) SqlInt64Array {
|
func NewSqlInt64Array(v []int64) SqlInt64Array {
|
||||||
return SqlInt64Array{Val: v, Valid: true}
|
return SqlInt64Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -440,6 +614,32 @@ func (a *SqlFloat32Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat32Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat32Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[float32](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat32Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat32Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[float32](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlFloat32Array(v []float32) SqlFloat32Array {
|
func NewSqlFloat32Array(v []float32) SqlFloat32Array {
|
||||||
return SqlFloat32Array{Val: v, Valid: true}
|
return SqlFloat32Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -515,6 +715,32 @@ func (a *SqlFloat64Array) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat64Array) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat64Array) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[float64](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlFloat64Array) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlFloat64Array) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[float64](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlFloat64Array(v []float64) SqlFloat64Array {
|
func NewSqlFloat64Array(v []float64) SqlFloat64Array {
|
||||||
return SqlFloat64Array{Val: v, Valid: true}
|
return SqlFloat64Array{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -591,6 +817,32 @@ func (a *SqlBoolArray) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlBoolArray) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlBoolArray) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[bool](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlBoolArray) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlBoolArray) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[bool](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlBoolArray(v []bool) SqlBoolArray {
|
func NewSqlBoolArray(v []bool) SqlBoolArray {
|
||||||
return SqlBoolArray{Val: v, Valid: true}
|
return SqlBoolArray{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -666,6 +918,32 @@ func (a *SqlUUIDArray) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a SqlUUIDArray) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlUUIDArray) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[uuid.UUID](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SqlUUIDArray) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, a.Valid, a.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SqlUUIDArray) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[uuid.UUID](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Val, a.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlUUIDArray(v []uuid.UUID) SqlUUIDArray {
|
func NewSqlUUIDArray(v []uuid.UUID) SqlUUIDArray {
|
||||||
return SqlUUIDArray{Val: v, Valid: true}
|
return SqlUUIDArray{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
@@ -750,6 +1028,32 @@ func (v *SqlVector) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v SqlVector) MarshalYAML() (any, error) {
|
||||||
|
return marshalYAMLSlice(v.Valid, v.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SqlVector) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
vals, valid, err := unmarshalYAMLSlice[float32](value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Val, v.Valid = vals, valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v SqlVector) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
return marshalXMLSlice(e, start, v.Valid, v.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *SqlVector) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
vals, err := unmarshalXMLSlice[float32](d, start)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Val, v.Valid = vals, true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlVector(val []float32) SqlVector {
|
func NewSqlVector(val []float32) SqlVector {
|
||||||
return SqlVector{Val: val, Valid: true}
|
return SqlVector{Val: val, Valid: true}
|
||||||
}
|
}
|
||||||
+331
-8
@@ -1,11 +1,12 @@
|
|||||||
// Package spectypes provides nullable SQL types with automatic casting and conversion methods.
|
// Package sqltypes provides nullable SQL types with automatic casting and conversion methods.
|
||||||
package spectypes
|
package sqltypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tryParseDT attempts to parse a string into a time.Time using various formats.
|
// tryParseDT attempts to parse a string into a time.Time using various formats.
|
||||||
@@ -120,15 +122,22 @@ func (n *SqlNull[T]) FromString(s string) error {
|
|||||||
|
|
||||||
var zero T
|
var zero T
|
||||||
switch any(zero).(type) {
|
switch any(zero).(type) {
|
||||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
case int, int8, int16, int32, int64:
|
||||||
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||||
reflect.ValueOf(&n.Val).Elem().SetInt(i)
|
reflect.ValueOf(&n.Val).Elem().SetInt(i)
|
||||||
n.Valid = true
|
n.Valid = true
|
||||||
}
|
} else if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
|
||||||
reflect.ValueOf(&n.Val).Elem().SetInt(int64(f))
|
reflect.ValueOf(&n.Val).Elem().SetInt(int64(f))
|
||||||
n.Valid = true
|
n.Valid = true
|
||||||
}
|
}
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
if u, err := strconv.ParseUint(s, 10, 64); err == nil {
|
||||||
|
reflect.ValueOf(&n.Val).Elem().SetUint(u)
|
||||||
|
n.Valid = true
|
||||||
|
} else if f, err := strconv.ParseFloat(s, 64); err == nil && f >= 0 {
|
||||||
|
reflect.ValueOf(&n.Val).Elem().SetUint(uint64(f))
|
||||||
|
n.Valid = true
|
||||||
|
}
|
||||||
case float32, float64:
|
case float32, float64:
|
||||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
reflect.ValueOf(&n.Val).Elem().SetFloat(f)
|
reflect.ValueOf(&n.Val).Elem().SetFloat(f)
|
||||||
@@ -232,6 +241,104 @@ func (n *SqlNull[T]) UnmarshalJSON(b []byte) error {
|
|||||||
return fmt.Errorf("cannot unmarshal %s into SqlNull[%T]", b, n.Val)
|
return fmt.Errorf("cannot unmarshal %s into SqlNull[%T]", b, n.Val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML implements yaml.Marshaler.
|
||||||
|
func (n SqlNull[T]) MarshalYAML() (any, error) {
|
||||||
|
if !n.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if T is []byte, and encode to base64 (mirrors MarshalJSON).
|
||||||
|
if b, ok := any(n.Val).([]byte); ok {
|
||||||
|
return base64.StdEncoding.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.Val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements yaml.Unmarshaler.
|
||||||
|
func (n *SqlNull[T]) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if value == nil || value.Tag == "!!null" {
|
||||||
|
n.Valid = false
|
||||||
|
n.Val = *new(T)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if T is []byte, and decode from base64.
|
||||||
|
var zero T
|
||||||
|
if _, ok := any(zero).([]byte); ok {
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err == nil {
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
|
||||||
|
n.Val = any(decoded).(T)
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n.Val = any([]byte(s)).(T)
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var val T
|
||||||
|
if err := value.Decode(&val); err == nil {
|
||||||
|
n.Val = val
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: decode as string and parse.
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err == nil {
|
||||||
|
return n.FromString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot unmarshal %q into SqlNull[%T]", value.Value, n.Val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalXML implements xml.Marshaler.
|
||||||
|
func (n SqlNull[T]) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if !n.Valid {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if T is []byte, and encode to base64 (mirrors MarshalJSON).
|
||||||
|
if b, ok := any(n.Val).([]byte); ok {
|
||||||
|
return e.EncodeElement(base64.StdEncoding.EncodeToString(b), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.EncodeElement(n.Val, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML implements xml.Unmarshaler.
|
||||||
|
//
|
||||||
|
// XML has no native null representation, so an empty element unmarshals to
|
||||||
|
// an invalid (null) value rather than a zero-value-but-valid one.
|
||||||
|
func (n *SqlNull[T]) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
n.Valid = false
|
||||||
|
n.Val = *new(T)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var zero T
|
||||||
|
if _, ok := any(zero).([]byte); ok {
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
|
||||||
|
n.Val = any(decoded).(T)
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n.Val = any([]byte(s)).(T)
|
||||||
|
n.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.FromString(s)
|
||||||
|
}
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
// String implements fmt.Stringer.
|
||||||
func (n SqlNull[T]) String() string {
|
func (n SqlNull[T]) String() string {
|
||||||
if !n.Valid {
|
if !n.Valid {
|
||||||
@@ -329,6 +436,7 @@ type (
|
|||||||
SqlInt16 = SqlNull[int16]
|
SqlInt16 = SqlNull[int16]
|
||||||
SqlInt32 = SqlNull[int32]
|
SqlInt32 = SqlNull[int32]
|
||||||
SqlInt64 = SqlNull[int64]
|
SqlInt64 = SqlNull[int64]
|
||||||
|
SqlFloat32 = SqlNull[float32]
|
||||||
SqlFloat64 = SqlNull[float64]
|
SqlFloat64 = SqlNull[float64]
|
||||||
SqlBool = SqlNull[bool]
|
SqlBool = SqlNull[bool]
|
||||||
SqlString = SqlNull[string]
|
SqlString = SqlNull[string]
|
||||||
@@ -343,7 +451,7 @@ func (t SqlTimeStamp) MarshalJSON() ([]byte, error) {
|
|||||||
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
return []byte(fmt.Sprintf(`"%s"`, t.Val.Format("2006-01-02T15:04:05"))), nil
|
return fmt.Appendf(nil, `"%s"`, t.Val.Format("2006-01-02T15:04:05")), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SqlTimeStamp) UnmarshalJSON(b []byte) error {
|
func (t *SqlTimeStamp) UnmarshalJSON(b []byte) error {
|
||||||
@@ -363,6 +471,49 @@ func (t SqlTimeStamp) Value() (driver.Value, error) {
|
|||||||
return t.Val.Format("2006-01-02T15:04:05"), nil
|
return t.Val.Format("2006-01-02T15:04:05"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t SqlTimeStamp) MarshalYAML() (any, error) {
|
||||||
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return t.Val.Format("2006-01-02T15:04:05"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SqlTimeStamp) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if err := t.SqlNull.UnmarshalYAML(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.Valid && (t.Val.IsZero() || t.Val.Format("2006-01-02T15:04:05") == "0001-01-01T00:00:00") {
|
||||||
|
t.Valid = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t SqlTimeStamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if !t.Valid || t.Val.IsZero() || t.Val.Before(time.Date(0002, 1, 1, 0, 0, 0, 0, time.UTC)) {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
return e.EncodeElement(t.Val.Format("2006-01-02T15:04:05"), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SqlTimeStamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
t.Valid = false
|
||||||
|
t.Val = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tm, err := tryParseDT(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Val = tm
|
||||||
|
t.Valid = !tm.IsZero() && tm.Format("2006-01-02T15:04:05") != "0001-01-01T00:00:00"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SqlTimeStampNow() SqlTimeStamp {
|
func SqlTimeStampNow() SqlTimeStamp {
|
||||||
return SqlTimeStamp{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
return SqlTimeStamp{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||||
}
|
}
|
||||||
@@ -378,7 +529,7 @@ func (d SqlDate) MarshalJSON() ([]byte, error) {
|
|||||||
if strings.HasPrefix(s, "0001-01-01") {
|
if strings.HasPrefix(s, "0001-01-01") {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
return []byte(fmt.Sprintf(`"%s"`, s)), nil
|
return fmt.Appendf(nil, `"%s"`, s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SqlDate) UnmarshalJSON(b []byte) error {
|
func (d *SqlDate) UnmarshalJSON(b []byte) error {
|
||||||
@@ -413,6 +564,57 @@ func (d SqlDate) String() string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d SqlDate) MarshalYAML() (any, error) {
|
||||||
|
if !d.Valid || d.Val.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
s := d.Val.Format("2006-01-02")
|
||||||
|
if strings.HasPrefix(s, "0001-01-01") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SqlDate) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if err := d.SqlNull.UnmarshalYAML(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.Valid && d.Val.Format("2006-01-02") <= "0001-01-01" {
|
||||||
|
d.Valid = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if !d.Valid || d.Val.IsZero() {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
s := d.Val.Format("2006-01-02")
|
||||||
|
if strings.HasPrefix(s, "0001-01-01") {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
return e.EncodeElement(s, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SqlDate) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := dec.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
d.Valid = false
|
||||||
|
d.Val = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tm, err := tryParseDT(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.Val = tm
|
||||||
|
d.Valid = !tm.IsZero() && tm.Format("2006-01-02") > "0001-01-01"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SqlDateNow() SqlDate {
|
func SqlDateNow() SqlDate {
|
||||||
return SqlDate{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
return SqlDate{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||||
}
|
}
|
||||||
@@ -428,7 +630,7 @@ func (t SqlTime) MarshalJSON() ([]byte, error) {
|
|||||||
if s == "00:00:00" {
|
if s == "00:00:00" {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
return []byte(fmt.Sprintf(`"%s"`, s)), nil
|
return fmt.Appendf(nil, `"%s"`, s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SqlTime) UnmarshalJSON(b []byte) error {
|
func (t *SqlTime) UnmarshalJSON(b []byte) error {
|
||||||
@@ -455,6 +657,57 @@ func (t SqlTime) String() string {
|
|||||||
return t.Val.Format("15:04:05")
|
return t.Val.Format("15:04:05")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t SqlTime) MarshalYAML() (any, error) {
|
||||||
|
if !t.Valid || t.Val.IsZero() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
s := t.Val.Format("15:04:05")
|
||||||
|
if s == "00:00:00" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SqlTime) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if err := t.SqlNull.UnmarshalYAML(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.Valid && t.Val.Format("15:04:05") == "00:00:00" {
|
||||||
|
t.Valid = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t SqlTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if !t.Valid || t.Val.IsZero() {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
s := t.Val.Format("15:04:05")
|
||||||
|
if s == "00:00:00" {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
return e.EncodeElement(s, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SqlTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
t.Valid = false
|
||||||
|
t.Val = time.Time{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tm, err := tryParseDT(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Val = tm
|
||||||
|
t.Valid = !tm.IsZero() && tm.Format("15:04:05") != "00:00:00"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SqlTimeNow() SqlTime {
|
func SqlTimeNow() SqlTime {
|
||||||
return SqlTime{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
return SqlTime{SqlNull: SqlNull[time.Time]{Val: time.Now(), Valid: true}}
|
||||||
}
|
}
|
||||||
@@ -462,6 +715,11 @@ func SqlTimeNow() SqlTime {
|
|||||||
// SqlJSONB - Nullable JSONB as []byte.
|
// SqlJSONB - Nullable JSONB as []byte.
|
||||||
type SqlJSONB []byte
|
type SqlJSONB []byte
|
||||||
|
|
||||||
|
// SqlJSON - Nullable JSON as []byte. PostgreSQL's json and jsonb types share
|
||||||
|
// the same textual representation and Go marshalling behavior, differing only
|
||||||
|
// in server-side storage, so SqlJSON is an alias of SqlJSONB.
|
||||||
|
type SqlJSON = SqlJSONB
|
||||||
|
|
||||||
// Scan implements sql.Scanner.
|
// Scan implements sql.Scanner.
|
||||||
func (n *SqlJSONB) Scan(value any) error {
|
func (n *SqlJSONB) Scan(value any) error {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
@@ -518,6 +776,67 @@ func (n *SqlJSONB) UnmarshalJSON(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML implements yaml.Marshaler. The underlying JSON is decoded into
|
||||||
|
// a generic value first so it renders as native YAML mappings/sequences
|
||||||
|
// rather than an embedded JSON string.
|
||||||
|
func (n SqlJSONB) MarshalYAML() (any, error) {
|
||||||
|
if len(n) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal(n, &v); err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements yaml.Unmarshaler.
|
||||||
|
func (n *SqlJSONB) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if value == nil || value.Tag == "!!null" {
|
||||||
|
*n = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := value.Decode(&v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*n = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalXML implements xml.Marshaler. JSON has no clean structural mapping
|
||||||
|
// to XML, so the raw JSON text is emitted as the element's text content.
|
||||||
|
func (n SqlJSONB) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if len(n) == 0 {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
var obj any
|
||||||
|
if err := json.Unmarshal(n, &obj); err != nil {
|
||||||
|
return e.EncodeElement("", start)
|
||||||
|
}
|
||||||
|
return e.EncodeElement(string(n), start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML implements xml.Unmarshaler, reading back the raw JSON text
|
||||||
|
// written by MarshalXML.
|
||||||
|
func (n *SqlJSONB) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
var s string
|
||||||
|
if err := d.DecodeElement(&s, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
*n = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*n = []byte(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (n SqlJSONB) AsMap() (map[string]any, error) {
|
func (n SqlJSONB) AsMap() (map[string]any, error) {
|
||||||
if len(n) == 0 {
|
if len(n) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -625,6 +944,10 @@ func NewSqlInt64(v int64) SqlInt64 {
|
|||||||
return SqlInt64{Val: v, Valid: true}
|
return SqlInt64{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewSqlFloat32(v float32) SqlFloat32 {
|
||||||
|
return SqlFloat32{Val: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlFloat64(v float64) SqlFloat64 {
|
func NewSqlFloat64(v float64) SqlFloat64 {
|
||||||
return SqlFloat64{Val: v, Valid: true}
|
return SqlFloat64{Val: v, Valid: true}
|
||||||
}
|
}
|
||||||
+21
-14
@@ -115,32 +115,39 @@ func GetHeadSpecHeaders() []string {
|
|||||||
|
|
||||||
// SetCORSHeaders sets CORS headers on a response writer
|
// SetCORSHeaders sets CORS headers on a response writer
|
||||||
func SetCORSHeaders(w ResponseWriter, r Request, config CORSConfig) {
|
func SetCORSHeaders(w ResponseWriter, r Request, config CORSConfig) {
|
||||||
// Set allowed origins
|
// Reflect the request origin; fall back to wildcard only when no origin is present
|
||||||
// if len(config.AllowedOrigins) > 0 {
|
origin := r.Header("Origin")
|
||||||
// w.SetHeader("Access-Control-Allow-Origin", strings.Join(config.AllowedOrigins, ", "))
|
if origin == "" {
|
||||||
// }
|
origin = "*"
|
||||||
|
} else {
|
||||||
// Todo origin list parsing
|
// Vary must be set so caches don't serve one origin's response to another
|
||||||
w.SetHeader("Access-Control-Allow-Origin", "*")
|
httpW := w.UnderlyingResponseWriter()
|
||||||
|
httpW.Header().Set("Vary", "Origin")
|
||||||
|
}
|
||||||
|
w.SetHeader("Access-Control-Allow-Origin", origin)
|
||||||
|
|
||||||
// Set allowed methods
|
// Set allowed methods
|
||||||
if len(config.AllowedMethods) > 0 {
|
if len(config.AllowedMethods) > 0 {
|
||||||
w.SetHeader("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", "))
|
w.SetHeader("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set allowed headers
|
// Reflect the preflight request headers when present; otherwise use the explicit config list
|
||||||
// if len(config.AllowedHeaders) > 0 {
|
requestedHeaders := r.Header("Access-Control-Request-Headers")
|
||||||
// w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", "))
|
if requestedHeaders != "" {
|
||||||
// }
|
w.SetHeader("Access-Control-Allow-Headers", requestedHeaders)
|
||||||
w.SetHeader("Access-Control-Allow-Headers", "*")
|
} else if len(config.AllowedHeaders) > 0 {
|
||||||
|
w.SetHeader("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
// Set max age
|
// Set max age
|
||||||
if config.MaxAge > 0 {
|
if config.MaxAge > 0 {
|
||||||
w.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge))
|
w.SetHeader("Access-Control-Max-Age", fmt.Sprintf("%d", config.MaxAge))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow credentials
|
// Allow credentials only when a specific origin is reflected (not wildcard)
|
||||||
w.SetHeader("Access-Control-Allow-Credentials", "true")
|
if origin != "*" {
|
||||||
|
w.SetHeader("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
// Expose headers that clients can read
|
// Expose headers that clients can read
|
||||||
exposeHeaders := config.AllowedHeaders
|
exposeHeaders := config.AllowedHeaders
|
||||||
|
|||||||
+4
@@ -50,6 +50,10 @@ type ServerInstanceConfig struct {
|
|||||||
// GZIP enables GZIP compression middleware
|
// GZIP enables GZIP compression middleware
|
||||||
GZIP bool `mapstructure:"gzip"`
|
GZIP bool `mapstructure:"gzip"`
|
||||||
|
|
||||||
|
// HTTP2 enables HTTP/2 with the Extended CONNECT protocol (RFC 8441) for WebSocket support.
|
||||||
|
// Requires TLS; pair with SSLCert/SSLKey, SelfSignedSSL, or AutoTLS.
|
||||||
|
HTTP2 bool `mapstructure:"http2"`
|
||||||
|
|
||||||
// TLS/HTTPS configuration options (mutually exclusive)
|
// TLS/HTTPS configuration options (mutually exclusive)
|
||||||
// Option 1: Provide certificate and key files directly
|
// Option 1: Provide certificate and key files directly
|
||||||
SSLCert string `mapstructure:"ssl_cert"`
|
SSLCert string `mapstructure:"ssl_cert"`
|
||||||
|
|||||||
+11
@@ -78,6 +78,17 @@ func (s *securityContext) GetUserID() (int, bool) {
|
|||||||
return security.GetUserID(s.ctx.Context)
|
return security.GetUserID(s.ctx.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserRef returns an opaque user identifier for row security lookups.
|
||||||
|
// It prefers the full *security.UserContext (so providers can read JWT claims,
|
||||||
|
// e.g. a UUID subject) and falls back to the int user ID.
|
||||||
|
func (s *securityContext) GetUserRef() (any, bool) {
|
||||||
|
if userCtx, ok := security.GetUserContext(s.ctx.Context); ok {
|
||||||
|
return userCtx, true
|
||||||
|
}
|
||||||
|
userID, ok := security.GetUserID(s.ctx.Context)
|
||||||
|
return userID, ok
|
||||||
|
}
|
||||||
|
|
||||||
func (s *securityContext) GetSchema() string {
|
func (s *securityContext) GetSchema() string {
|
||||||
return s.ctx.Schema
|
return s.ctx.Schema
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-1
@@ -11,7 +11,8 @@ Type-safe, composable security system for ResolveSpec with support for authentic
|
|||||||
- ✅ **No Global State** - Each handler has its own security configuration
|
- ✅ **No Global State** - Each handler has its own security configuration
|
||||||
- ✅ **Testable** - Easy to mock and test
|
- ✅ **Testable** - Easy to mock and test
|
||||||
- ✅ **Extensible** - Implement custom providers for your needs
|
- ✅ **Extensible** - Implement custom providers for your needs
|
||||||
- ✅ **Stored Procedures** - All database operations use PostgreSQL stored procedures for security and maintainability
|
- ✅ **Stored Procedures** - Database operations use PostgreSQL stored procedures where available, for security and maintainability
|
||||||
|
- ✅ **Direct Mode** - Portable Go/SQL fallback for SQLite, MySQL, or Postgres without the stored procedures installed — no code changes required
|
||||||
- ✅ **OAuth2 Authorization Server** - Built-in OAuth 2.1 + PKCE server (RFC 8414, 7591, 7009, 7662) with login form and external provider federation
|
- ✅ **OAuth2 Authorization Server** - Built-in OAuth 2.1 + PKCE server (RFC 8414, 7591, 7009, 7662) with login form and external provider federation
|
||||||
- ✅ **Password Reset** - Self-service password reset with secure token generation and session invalidation
|
- ✅ **Password Reset** - Self-service password reset with secure token generation and session invalidation
|
||||||
|
|
||||||
@@ -51,6 +52,94 @@ Type-safe, composable security system for ResolveSpec with support for authentic
|
|||||||
|
|
||||||
See `database_schema.sql` for complete stored procedure definitions and examples.
|
See `database_schema.sql` for complete stored procedure definitions and examples.
|
||||||
|
|
||||||
|
**Not on Postgres, or don't have the procedures installed?** See [Direct Mode](#direct-mode-portable-sql-without-stored-procedures) below — every provider that calls a `resolvespec_*` procedure also has a portable Go/SQL implementation that works on SQLite, MySQL, or plain Postgres.
|
||||||
|
|
||||||
|
## Direct Mode (portable SQL without stored procedures)
|
||||||
|
|
||||||
|
Every database-backed provider (`DatabaseAuthenticator`, `JWTAuthenticator`, `DatabaseTwoFactorProvider`, `DatabasePasskeyProvider`, the OAuth2 methods/server, `DatabaseKeyStore`) has two code paths:
|
||||||
|
|
||||||
|
- **Procedure mode** — calls the configured `resolvespec_*` stored procedure (original behavior, Postgres-only).
|
||||||
|
- **Direct mode** — reimplements the same logic in Go using plain parameterized SQL against configurable table names. Works on SQLite, MySQL, or a Postgres database where the procedures were never deployed.
|
||||||
|
|
||||||
|
### QueryMode
|
||||||
|
|
||||||
|
Selection is controlled per-provider by a `QueryMode`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type QueryMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeAuto QueryMode = iota // default
|
||||||
|
ModeProcedure
|
||||||
|
ModeDirect
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`ModeAuto`** (default, zero value) — auto-detects per connection:
|
||||||
|
- SQLite/MySQL drivers → Direct mode, no probing.
|
||||||
|
- Postgres drivers (`lib/pq`, `pgx`) → probes `pg_proc` for the configured procedure name and uses it **only if it actually exists**; otherwise falls back to Direct mode. The result is cached per procedure name and reset on reconnect.
|
||||||
|
- Any other/unrecognized driver (including `sqlmock` test doubles) → defaults to Procedure mode, preserving existing behavior for callers that don't expose an identifiable driver type.
|
||||||
|
- **`ModeProcedure`** — always calls the stored procedure, regardless of dialect.
|
||||||
|
- **`ModeDirect`** — always uses the portable Go/SQL path, never the stored procedure.
|
||||||
|
|
||||||
|
Set it via the provider's `Options` struct or `With...` chain method:
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth := security.NewDatabaseAuthenticatorWithOptions(db, security.DatabaseAuthenticatorOptions{
|
||||||
|
QueryMode: security.ModeDirect, // force Direct mode, e.g. for SQLite
|
||||||
|
})
|
||||||
|
|
||||||
|
tfaProvider := security.NewDatabaseTwoFactorProvider(sqliteDB, nil).
|
||||||
|
WithQueryMode(security.ModeDirect)
|
||||||
|
```
|
||||||
|
|
||||||
|
On a real SQLite/MySQL connection you can usually leave `QueryMode` unset — `ModeAuto` detects the dialect and uses Direct mode automatically.
|
||||||
|
|
||||||
|
### TableNames / KeyStoreTableNames
|
||||||
|
|
||||||
|
Direct mode reads/writes plain tables instead of calling procedures, so table names are configurable the same way procedure names are (`SQLNames`):
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TableNames struct {
|
||||||
|
Users string // default: "users"
|
||||||
|
UserSessions string // default: "user_sessions"
|
||||||
|
TokenBlacklist string // default: "token_blacklist"
|
||||||
|
UserTOTPBackupCodes string // default: "user_totp_backup_codes"
|
||||||
|
UserPasskeyCredentials string // default: "user_passkey_credentials"
|
||||||
|
UserPasswordResets string // default: "user_password_resets"
|
||||||
|
OAuthClients string // default: "oauth_clients"
|
||||||
|
OAuthCodes string // default: "oauth_codes"
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyStoreTableNames struct {
|
||||||
|
UserKeys string // default: "user_keys" — used by DatabaseKeyStore
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`DefaultTableNames()` / `MergeTableNames()` / `ValidateTableNames()` mirror `DefaultSQLNames()` / `MergeSQLNames()` / `ValidateSQLNames()`. Set custom names via the same `Options`/`With...` surface as `QueryMode`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth := security.NewDatabaseAuthenticatorWithOptions(db, security.DatabaseAuthenticatorOptions{
|
||||||
|
TableNames: &security.TableNames{Users: "app_users"}, // only override what differs
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`oauth2_methods.go` and `oauth_server_db.go` are methods on `*DatabaseAuthenticator` and reuse its `TableNames`/`QueryMode`; there's no separate config for them.
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
`database_schema_sqlite.sql` is the portable companion to `database_schema.sql` — plain `CREATE TABLE` statements only (no functions, no triggers, no `jsonb`/`bytea`/array types), covering every table Direct mode reads or writes. Use it to stand up a SQLite (or adapt for MySQL) database for Direct mode.
|
||||||
|
|
||||||
|
### What's NOT covered
|
||||||
|
|
||||||
|
`ColumnSecurityProvider`/`RowSecurityProvider` (`resolvespec_column_security` / `resolvespec_row_security`) query an external `core.secaccess`/`core.hub_link` schema this package doesn't own. Direct mode has no portable equivalent to fabricate for these and returns `security.ErrDirectModeUnsupported` — use `ConfigColumnSecurityProvider`/`ConfigRowSecurityProvider` instead when not running against Postgres with those procedures installed.
|
||||||
|
|
||||||
|
### Behavioral notes
|
||||||
|
|
||||||
|
- Direct mode matches Procedure mode's current behavior exactly, including its TODOs — e.g. passwords are compared as-is (the stored procedures don't verify bcrypt hashes yet either; see the TODO in `resolvespec_login`/`resolvespec_password_reset`).
|
||||||
|
- Session tokens generated by Direct mode use the same `sess_<hex>_<unix-timestamp>` shape as the plpgsql procedures.
|
||||||
|
- `bytea`/array/`jsonb` Postgres-only columns (passkey credentials, OAuth2 client scopes, keystore `meta`) are stored as base64/JSON-encoded `TEXT` in Direct mode — transparent to callers, since the Go-level API already deals in those same encodings.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user