@open-mercato/ai-assistant 0.6.6-develop.6465.1.019f0fb26f → 0.6.6-develop.6471.1.9ab5bdd79a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frontend/hooks/useCommandPalette.js +16 -14
- package/dist/frontend/hooks/useCommandPalette.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +5 -3
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +4 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js +5 -3
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js +4 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +6 -7
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +5 -3
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/import/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/conversations/import/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/route.js +4 -2
- package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/run-object/route.js +3 -1
- package/dist/modules/ai_assistant/api/ai/run-object/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/chat/route.js +23 -14
- package/dist/modules/ai_assistant/api/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/health/route.js +3 -1
- package/dist/modules/ai_assistant/api/health/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/mcp-key/route.js +3 -1
- package/dist/modules/ai_assistant/api/mcp-key/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/route/route.js +11 -9
- package/dist/modules/ai_assistant/api/route/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/session-key/route.js +3 -1
- package/dist/modules/ai_assistant/api/session-key/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +4 -2
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +6 -4
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/tools/execute/route.js +3 -1
- package/dist/modules/ai_assistant/api/tools/execute/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/tools/route.js +3 -1
- package/dist/modules/ai_assistant/api/tools/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/daily/route.js +3 -1
- package/dist/modules/ai_assistant/api/usage/daily/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +3 -1
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/sessions/route.js +3 -1
- package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +2 -2
- package/dist/modules/ai_assistant/cli.js +3 -0
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-policy.js +7 -5
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-registry.js +11 -13
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +21 -44
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-tools.js +5 -10
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-overrides.js +5 -9
- package/dist/modules/ai_assistant/lib/ai-overrides.js.map +2 -2
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js +22 -22
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +2 -2
- package/dist/modules/ai_assistant/lib/attachment-parts.js +10 -26
- package/dist/modules/ai_assistant/lib/attachment-parts.js.map +2 -2
- package/dist/modules/ai_assistant/lib/auth.js +3 -1
- package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
- package/dist/modules/ai_assistant/lib/codemode-tools.js +15 -17
- package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/entity-index.js +7 -5
- package/dist/modules/ai_assistant/lib/entity-index.js.map +2 -2
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js +3 -4
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/http-server.js +45 -48
- package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-bootstrap.js +5 -12
- package/dist/modules/ai_assistant/lib/llm-bootstrap.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-client.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-server.js +24 -19
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js +3 -1
- package/dist/modules/ai_assistant/lib/mcp-tool-adapter.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +4 -2
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/opencode-client.js +5 -3
- package/dist/modules/ai_assistant/lib/opencode-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/opencode-handlers.js +30 -12
- package/dist/modules/ai_assistant/lib/opencode-handlers.js.map +2 -2
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js +3 -1
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +2 -2
- package/dist/modules/ai_assistant/lib/pending-action-executor.js +3 -1
- package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +2 -2
- package/dist/modules/ai_assistant/lib/prepare-mutation.js +4 -6
- package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +2 -2
- package/dist/modules/ai_assistant/lib/schema-utils.js +3 -1
- package/dist/modules/ai_assistant/lib/schema-utils.js.map +2 -2
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js +4 -8
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-executor.js +3 -1
- package/dist/modules/ai_assistant/lib/tool-executor.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-loader.js +18 -25
- package/dist/modules/ai_assistant/lib/tool-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-registry.js +3 -1
- package/dist/modules/ai_assistant/lib/tool-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-test-runner.js +3 -1
- package/dist/modules/ai_assistant/lib/tool-test-runner.js.map +2 -2
- package/dist/modules/ai_assistant/setup.js +4 -8
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js +3 -1
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js +7 -19
- package/dist/modules/ai_assistant/workers/ai-pending-action-cleanup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +5 -11
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +2 -2
- package/package.json +6 -6
- package/src/frontend/hooks/useCommandPalette.ts +17 -16
- package/src/modules/ai_assistant/__tests__/events.test.ts +23 -5
- package/src/modules/ai_assistant/__tests__/integration/ws-c-attachment-bridge.test.ts +21 -3
- package/src/modules/ai_assistant/__tests__/integration/ws-c-policy-and-tools.test.ts +22 -3
- package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +26 -5
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +6 -3
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +5 -2
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/mutation-policy/route.ts +6 -3
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/prompt-override/route.ts +5 -2
- package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/chat/route.ts +7 -7
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +6 -3
- package/src/modules/ai_assistant/api/ai/conversations/import/route.ts +4 -1
- package/src/modules/ai_assistant/api/ai/conversations/route.ts +5 -2
- package/src/modules/ai_assistant/api/ai/run-object/route.ts +4 -1
- package/src/modules/ai_assistant/api/chat/route.ts +24 -15
- package/src/modules/ai_assistant/api/health/route.ts +4 -1
- package/src/modules/ai_assistant/api/mcp-key/route.ts +4 -1
- package/src/modules/ai_assistant/api/route/route.ts +12 -9
- package/src/modules/ai_assistant/api/session-key/route.ts +4 -1
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +5 -2
- package/src/modules/ai_assistant/api/settings/route.ts +7 -4
- package/src/modules/ai_assistant/api/tools/execute/route.ts +4 -1
- package/src/modules/ai_assistant/api/tools/route.ts +4 -1
- package/src/modules/ai_assistant/api/usage/daily/route.ts +4 -1
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +4 -1
- package/src/modules/ai_assistant/api/usage/sessions/route.ts +4 -1
- package/src/modules/ai_assistant/cli.ts +5 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-policy.mutation-override.test.ts +20 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +25 -5
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +21 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-parity.test.ts +23 -3
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +20 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-tools.test.ts +22 -3
- package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +23 -3
- package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +25 -7
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +25 -5
- package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +19 -1
- package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +22 -3
- package/src/modules/ai_assistant/lib/agent-policy.ts +8 -5
- package/src/modules/ai_assistant/lib/agent-registry.ts +12 -15
- package/src/modules/ai_assistant/lib/agent-runtime.ts +23 -46
- package/src/modules/ai_assistant/lib/agent-tools.ts +7 -11
- package/src/modules/ai_assistant/lib/ai-overrides.ts +6 -9
- package/src/modules/ai_assistant/lib/api-endpoint-index.ts +23 -22
- package/src/modules/ai_assistant/lib/attachment-parts.ts +14 -29
- package/src/modules/ai_assistant/lib/auth.ts +4 -1
- package/src/modules/ai_assistant/lib/codemode-tools.ts +17 -17
- package/src/modules/ai_assistant/lib/entity-index.ts +8 -5
- package/src/modules/ai_assistant/lib/generated-registry-loader.ts +4 -4
- package/src/modules/ai_assistant/lib/http-server.ts +46 -48
- package/src/modules/ai_assistant/lib/llm-bootstrap.ts +7 -13
- package/src/modules/ai_assistant/lib/mcp-client.ts +4 -1
- package/src/modules/ai_assistant/lib/mcp-dev-key-resolution.ts +4 -1
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +4 -1
- package/src/modules/ai_assistant/lib/mcp-server.ts +27 -19
- package/src/modules/ai_assistant/lib/mcp-tool-adapter.ts +4 -1
- package/src/modules/ai_assistant/lib/model-factory.ts +6 -3
- package/src/modules/ai_assistant/lib/opencode-client.ts +6 -3
- package/src/modules/ai_assistant/lib/opencode-handlers.ts +32 -12
- package/src/modules/ai_assistant/lib/pending-action-cancel.ts +4 -1
- package/src/modules/ai_assistant/lib/pending-action-executor.ts +4 -1
- package/src/modules/ai_assistant/lib/prepare-mutation.ts +5 -6
- package/src/modules/ai_assistant/lib/schema-utils.ts +4 -1
- package/src/modules/ai_assistant/lib/token-usage-recorder.ts +5 -8
- package/src/modules/ai_assistant/lib/tool-executor.ts +4 -1
- package/src/modules/ai_assistant/lib/tool-loader.ts +19 -25
- package/src/modules/ai_assistant/lib/tool-registry.ts +4 -1
- package/src/modules/ai_assistant/lib/tool-test-runner.ts +4 -1
- package/src/modules/ai_assistant/setup.ts +5 -8
- package/src/modules/ai_assistant/subscribers/conversation-shared-notify.ts +4 -1
- package/src/modules/ai_assistant/workers/ai-pending-action-cleanup.ts +8 -19
- package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +6 -11
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
1
2
|
import { NextResponse } from "next/server";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
@@ -6,6 +7,7 @@ import { getAgent, loadAgentRegistry } from "../../../../../lib/agent-registry.j
|
|
|
6
7
|
import { hasRequiredFeatures } from "../../../../../lib/auth.js";
|
|
7
8
|
import { AiAgentPromptOverrideRepository } from "../../../../../data/repositories/AiAgentPromptOverrideRepository.js";
|
|
8
9
|
import { findReservedKeys } from "../../../../../lib/prompt-override-merge.js";
|
|
10
|
+
const logger = createLogger("ai_assistant");
|
|
9
11
|
const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/;
|
|
10
12
|
const agentIdParamSchema = z.object({
|
|
11
13
|
agentId: z.string().regex(agentIdPattern, 'agentId must match "<module>.<agent>" (lowercase, digits, underscores only)')
|
|
@@ -155,7 +157,7 @@ async function GET(req, context) {
|
|
|
155
157
|
versions: versions.map(serializeOverride)
|
|
156
158
|
});
|
|
157
159
|
} catch (error) {
|
|
158
|
-
|
|
160
|
+
logger.error("AI Prompt Override GET \u2014 Failure", { err: error });
|
|
159
161
|
return jsonError(
|
|
160
162
|
500,
|
|
161
163
|
error instanceof Error ? error.message : "Failed to load prompt override.",
|
|
@@ -229,7 +231,7 @@ async function POST(req, context) {
|
|
|
229
231
|
updatedAt: saved.updatedAt?.toISOString?.() ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
230
232
|
});
|
|
231
233
|
} catch (error) {
|
|
232
|
-
|
|
234
|
+
logger.error("AI Prompt Override POST \u2014 Failure", { err: error });
|
|
233
235
|
return jsonError(
|
|
234
236
|
500,
|
|
235
237
|
error instanceof Error ? error.message : "Failed to save prompt override.",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/agents/%5BagentId%5D/prompt-override/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { AiAgentPromptOverrideRepository } from '../../../../../data/repositories/AiAgentPromptOverrideRepository'\nimport type { AiAgentPromptOverride } from '../../../../../data/entities'\nimport { findReservedKeys } from '../../../../../lib/prompt-override-merge'\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst agentIdParamSchema = z.object({\n agentId: z\n .string()\n .regex(agentIdPattern, 'agentId must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n})\n\nconst sectionsSchema = z.record(z.string(), z.string())\n\n/**\n * Accept both `sections` (Step 5.3 canonical shape) and the Step 4.5\n * placeholder `overrides` key so UI callers that haven't redeployed still\n * work. `sections` wins when both are present.\n */\nconst promptOverrideRequestSchema = z\n .object({\n sections: sectionsSchema.optional(),\n overrides: sectionsSchema.optional(),\n notes: z.string().max(2000).optional(),\n })\n .refine((value) => value.sections !== undefined || value.overrides !== undefined, {\n message: 'Body must include either `sections` or `overrides`.',\n })\n\nexport type AiPromptOverrideRequest = z.infer<typeof promptOverrideRequestSchema>\n\nconst REQUIRED_FEATURE = 'ai_assistant.settings.manage'\nconst HISTORY_LIMIT = 10\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Versioned additive prompt-overrides for an AI agent',\n methods: {\n GET: {\n operationId: 'aiAssistantGetPromptOverride',\n summary: 'Read the latest prompt-section override for an agent plus recent version history.',\n description:\n 'Returns `{ agentId, override, versions }` where `override` is the latest persisted ' +\n 'row (or `null`) and `versions` is the newest-first history capped at 10 rows. ' +\n 'Tenant-scoped; requires `ai_assistant.settings.manage`.',\n responses: [\n {\n status: 200,\n description: 'Latest override + recent version history.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid agent id.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },\n { status: 404, description: 'Unknown agent id.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantSavePromptOverride',\n summary: 'Save a new prompt-section override version for the agent.',\n description:\n 'Persists an additive `{ sections: Record<sectionId, text>, notes? }` override, allocating ' +\n 'the next monotonic version for `(tenant, org, agent)`. Reserved policy keys ' +\n '(`mutationPolicy`, `readOnly`, `allowedTools`, `acceptedMediaTypes`) are rejected with ' +\n '400 / `reserved_key`. Requires `ai_assistant.settings.manage`.',\n requestBody: {\n contentType: 'application/json',\n description: 'Body: `{ sections: Record<string, string>, notes?: string }`.',\n schema: promptOverrideRequestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Override persisted. Returns `{ ok: true, version, updatedAt }`.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid agent id, malformed body, or reserved policy key.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },\n { status: 404, description: 'Unknown agent id.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: [REQUIRED_FEATURE],\n}\n\ninterface RouteContext {\n params: Promise<{ agentId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\ninterface ResolvedAuth {\n tenantId: string | null\n organizationId: string | null\n userId: string\n isSuperAdmin: boolean\n features: string[]\n container: Awaited<ReturnType<typeof createRequestContainer>>\n rbacService: RbacService\n}\n\nasync function resolveAuthOrRespond(\n req: NextRequest,\n): Promise<ResolvedAuth | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n isSuperAdmin: acl.isSuperAdmin,\n features: acl.features,\n container,\n rbacService,\n }\n}\n\nfunction serializeOverride(row: AiAgentPromptOverride) {\n return {\n id: row.id,\n agentId: row.agentId,\n version: row.version,\n sections: row.sections ?? {},\n notes: row.notes ?? null,\n createdByUserId: row.createdByUserId ?? null,\n createdAt: row.createdAt?.toISOString?.() ?? new Date().toISOString(),\n updatedAt: row.updatedAt?.toISOString?.() ?? new Date().toISOString(),\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const authResult = await resolveAuthOrRespond(req)\n if (authResult instanceof NextResponse) return authResult\n\n const rawParams = await context.params\n const paramResult = agentIdParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return jsonError(400, 'Invalid agent id.', 'validation_error', {\n issues: paramResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n const agent = getAgent(paramResult.data.agentId)\n if (!agent) {\n return jsonError(404, `Unknown agent \"${paramResult.data.agentId}\".`, 'agent_unknown')\n }\n\n if (!authResult.tenantId) {\n return NextResponse.json({\n agentId: agent.id,\n override: null,\n versions: [],\n })\n }\n\n const em = authResult.container.resolve<EntityManager>('em')\n const repo = new AiAgentPromptOverrideRepository(em)\n const [latest, versions] = await Promise.all([\n repo.getLatest(agent.id, {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n }),\n repo.listVersions(\n agent.id,\n {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n },\n HISTORY_LIMIT,\n ),\n ])\n\n return NextResponse.json({\n agentId: agent.id,\n override: latest ? serializeOverride(latest) : null,\n versions: versions.map(serializeOverride),\n })\n } catch (error) {\n console.error('[AI Prompt Override GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load prompt override.',\n 'internal_error',\n )\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const authResult = await resolveAuthOrRespond(req)\n if (authResult instanceof NextResponse) return authResult\n\n const rawParams = await context.params\n const paramResult = agentIdParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return jsonError(400, 'Invalid agent id.', 'validation_error', {\n issues: paramResult.error.issues,\n })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = promptOverrideRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n const sections = bodyResult.data.sections ?? bodyResult.data.overrides ?? {}\n const reservedHits = findReservedKeys(sections)\n if (reservedHits.length > 0) {\n return jsonError(\n 400,\n `Prompt override contains reserved policy keys: ${reservedHits.join(', ')}.`,\n 'reserved_key',\n { reservedKeys: reservedHits },\n )\n }\n\n try {\n await loadAgentRegistry()\n const agent = getAgent(paramResult.data.agentId)\n if (!agent) {\n return jsonError(404, `Unknown agent \"${paramResult.data.agentId}\".`, 'agent_unknown')\n }\n\n if (!authResult.tenantId) {\n return jsonError(\n 400,\n 'Caller has no tenant context; cannot persist tenant-scoped prompt override.',\n 'tenant_required',\n )\n }\n\n const em = authResult.container.resolve<EntityManager>('em')\n const repo = new AiAgentPromptOverrideRepository(em)\n const saved = await repo.save(\n {\n agentId: agent.id,\n sections,\n notes: bodyResult.data.notes ?? null,\n },\n {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n userId: authResult.userId,\n },\n )\n\n return NextResponse.json({\n ok: true,\n agentId: agent.id,\n version: saved.version,\n updatedAt: saved.updatedAt?.toISOString?.() ?? new Date().toISOString(),\n })\n } catch (error) {\n console.error('[AI Prompt Override POST] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to save prompt override.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { AiAgentPromptOverrideRepository } from '../../../../../data/repositories/AiAgentPromptOverrideRepository'\nimport type { AiAgentPromptOverride } from '../../../../../data/entities'\nimport { findReservedKeys } from '../../../../../lib/prompt-override-merge'\n\nconst logger = createLogger('ai_assistant')\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst agentIdParamSchema = z.object({\n agentId: z\n .string()\n .regex(agentIdPattern, 'agentId must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n})\n\nconst sectionsSchema = z.record(z.string(), z.string())\n\n/**\n * Accept both `sections` (Step 5.3 canonical shape) and the Step 4.5\n * placeholder `overrides` key so UI callers that haven't redeployed still\n * work. `sections` wins when both are present.\n */\nconst promptOverrideRequestSchema = z\n .object({\n sections: sectionsSchema.optional(),\n overrides: sectionsSchema.optional(),\n notes: z.string().max(2000).optional(),\n })\n .refine((value) => value.sections !== undefined || value.overrides !== undefined, {\n message: 'Body must include either `sections` or `overrides`.',\n })\n\nexport type AiPromptOverrideRequest = z.infer<typeof promptOverrideRequestSchema>\n\nconst REQUIRED_FEATURE = 'ai_assistant.settings.manage'\nconst HISTORY_LIMIT = 10\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Versioned additive prompt-overrides for an AI agent',\n methods: {\n GET: {\n operationId: 'aiAssistantGetPromptOverride',\n summary: 'Read the latest prompt-section override for an agent plus recent version history.',\n description:\n 'Returns `{ agentId, override, versions }` where `override` is the latest persisted ' +\n 'row (or `null`) and `versions` is the newest-first history capped at 10 rows. ' +\n 'Tenant-scoped; requires `ai_assistant.settings.manage`.',\n responses: [\n {\n status: 200,\n description: 'Latest override + recent version history.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid agent id.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },\n { status: 404, description: 'Unknown agent id.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantSavePromptOverride',\n summary: 'Save a new prompt-section override version for the agent.',\n description:\n 'Persists an additive `{ sections: Record<sectionId, text>, notes? }` override, allocating ' +\n 'the next monotonic version for `(tenant, org, agent)`. Reserved policy keys ' +\n '(`mutationPolicy`, `readOnly`, `allowedTools`, `acceptedMediaTypes`) are rejected with ' +\n '400 / `reserved_key`. Requires `ai_assistant.settings.manage`.',\n requestBody: {\n contentType: 'application/json',\n description: 'Body: `{ sections: Record<string, string>, notes?: string }`.',\n schema: promptOverrideRequestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Override persisted. Returns `{ ok: true, version, updatedAt }`.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid agent id, malformed body, or reserved policy key.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },\n { status: 404, description: 'Unknown agent id.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: [REQUIRED_FEATURE],\n}\n\ninterface RouteContext {\n params: Promise<{ agentId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\ninterface ResolvedAuth {\n tenantId: string | null\n organizationId: string | null\n userId: string\n isSuperAdmin: boolean\n features: string[]\n container: Awaited<ReturnType<typeof createRequestContainer>>\n rbacService: RbacService\n}\n\nasync function resolveAuthOrRespond(\n req: NextRequest,\n): Promise<ResolvedAuth | NextResponse> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n return {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n isSuperAdmin: acl.isSuperAdmin,\n features: acl.features,\n container,\n rbacService,\n }\n}\n\nfunction serializeOverride(row: AiAgentPromptOverride) {\n return {\n id: row.id,\n agentId: row.agentId,\n version: row.version,\n sections: row.sections ?? {},\n notes: row.notes ?? null,\n createdByUserId: row.createdByUserId ?? null,\n createdAt: row.createdAt?.toISOString?.() ?? new Date().toISOString(),\n updatedAt: row.updatedAt?.toISOString?.() ?? new Date().toISOString(),\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const authResult = await resolveAuthOrRespond(req)\n if (authResult instanceof NextResponse) return authResult\n\n const rawParams = await context.params\n const paramResult = agentIdParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return jsonError(400, 'Invalid agent id.', 'validation_error', {\n issues: paramResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n const agent = getAgent(paramResult.data.agentId)\n if (!agent) {\n return jsonError(404, `Unknown agent \"${paramResult.data.agentId}\".`, 'agent_unknown')\n }\n\n if (!authResult.tenantId) {\n return NextResponse.json({\n agentId: agent.id,\n override: null,\n versions: [],\n })\n }\n\n const em = authResult.container.resolve<EntityManager>('em')\n const repo = new AiAgentPromptOverrideRepository(em)\n const [latest, versions] = await Promise.all([\n repo.getLatest(agent.id, {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n }),\n repo.listVersions(\n agent.id,\n {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n },\n HISTORY_LIMIT,\n ),\n ])\n\n return NextResponse.json({\n agentId: agent.id,\n override: latest ? serializeOverride(latest) : null,\n versions: versions.map(serializeOverride),\n })\n } catch (error) {\n logger.error('AI Prompt Override GET \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load prompt override.',\n 'internal_error',\n )\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const authResult = await resolveAuthOrRespond(req)\n if (authResult instanceof NextResponse) return authResult\n\n const rawParams = await context.params\n const paramResult = agentIdParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return jsonError(400, 'Invalid agent id.', 'validation_error', {\n issues: paramResult.error.issues,\n })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = promptOverrideRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n const sections = bodyResult.data.sections ?? bodyResult.data.overrides ?? {}\n const reservedHits = findReservedKeys(sections)\n if (reservedHits.length > 0) {\n return jsonError(\n 400,\n `Prompt override contains reserved policy keys: ${reservedHits.join(', ')}.`,\n 'reserved_key',\n { reservedKeys: reservedHits },\n )\n }\n\n try {\n await loadAgentRegistry()\n const agent = getAgent(paramResult.data.agentId)\n if (!agent) {\n return jsonError(404, `Unknown agent \"${paramResult.data.agentId}\".`, 'agent_unknown')\n }\n\n if (!authResult.tenantId) {\n return jsonError(\n 400,\n 'Caller has no tenant context; cannot persist tenant-scoped prompt override.',\n 'tenant_required',\n )\n }\n\n const em = authResult.container.resolve<EntityManager>('em')\n const repo = new AiAgentPromptOverrideRepository(em)\n const saved = await repo.save(\n {\n agentId: agent.id,\n sections,\n notes: bodyResult.data.notes ?? null,\n },\n {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n userId: authResult.userId,\n },\n )\n\n return NextResponse.json({\n ok: true,\n agentId: agent.id,\n version: saved.version,\n updatedAt: saved.updatedAt?.toISOString?.() ?? new Date().toISOString(),\n })\n } catch (error) {\n logger.error('AI Prompt Override POST \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to save prompt override.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
1
2
|
import { NextResponse } from "next/server";
|
|
2
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
@@ -5,6 +6,7 @@ import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-re
|
|
|
5
6
|
import { isAgentTaskPlanEnabled, listAgents, loadAgentRegistry } from "../../../lib/agent-registry.js";
|
|
6
7
|
import { hasRequiredFeatures } from "../../../lib/auth.js";
|
|
7
8
|
import { toolRegistry } from "../../../lib/tool-registry.js";
|
|
9
|
+
const logger = createLogger("ai_assistant");
|
|
8
10
|
const openApi = {
|
|
9
11
|
tag: "AI Assistant",
|
|
10
12
|
summary: "List AI agents the caller can invoke",
|
|
@@ -84,7 +86,7 @@ async function GET(req) {
|
|
|
84
86
|
});
|
|
85
87
|
return NextResponse.json({ agents, total: agents.length, aiConfigured });
|
|
86
88
|
} catch (error) {
|
|
87
|
-
|
|
89
|
+
logger.error("AI Agents \u2014 Failed to list agents", { err: error });
|
|
88
90
|
return NextResponse.json(
|
|
89
91
|
{ error: "Failed to list agents", code: "internal_error" },
|
|
90
92
|
{ status: 500 }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ai_assistant/api/ai/agents/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { isAgentTaskPlanEnabled, listAgents, loadAgentRegistry } from '../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport { toolRegistry } from '../../../lib/tool-registry'\nimport type { AiToolDefinition } from '../../../lib/types'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'List AI agents the caller can invoke',\n methods: {\n GET: {\n operationId: 'aiAssistantListAgents',\n summary: 'List registered AI agents, filtered by the caller\\'s features.',\n description:\n 'Returns `{ agents: [...] }` \u2014 the subset of agents from `ai-agents.generated.ts` that the ' +\n 'authenticated caller can invoke based on each agent\\'s `requiredFeatures`. Mirrors the ' +\n '`meta.list_agents` tool handler so backoffice pages (e.g. the playground) can render an ' +\n 'agent picker without going through the MCP tool transport.',\n responses: [\n {\n status: 200,\n description: 'Accessible agent summaries.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 500, description: 'Internal failure while loading the agent registry.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized', code: 'unauthenticated' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // No LLM provider configured (no API keys set). The launcher uses the\n // `aiConfigured` flag to show a setup prompt; explicit `<AiChat>` mounts\n // and playground pages still see the full registry so they can show their\n // own configuration prompts instead of silently disappearing.\n const aiConfigured = llmProviderRegistry.resolveFirstConfigured() != null\n\n await loadAgentRegistry()\n const all = listAgents()\n const accessible = all.filter((agent) =>\n hasRequiredFeatures(agent.requiredFeatures, acl.features, acl.isSuperAdmin, rbacService),\n )\n\n const agents = accessible.map((agent) => {\n const tools = agent.allowedTools.map((toolName) => {\n const tool = toolRegistry.getTool(toolName) as AiToolDefinition | undefined\n return {\n name: toolName,\n displayName: tool?.displayName ?? toolName,\n isMutation: Boolean(tool?.isMutation),\n registered: Boolean(tool),\n }\n })\n return {\n id: agent.id,\n moduleId: agent.moduleId,\n label: agent.label,\n description: agent.description,\n systemPrompt: agent.systemPrompt,\n executionMode: agent.executionMode ?? 'chat',\n defaultProvider: agent.defaultProvider ?? null,\n defaultModel: agent.defaultModel ?? null,\n defaultBaseUrl: agent.defaultBaseUrl ?? null,\n allowRuntimeModelOverride: agent.allowRuntimeModelOverride !== false,\n mutationPolicy: agent.mutationPolicy ?? 'read-only',\n readOnly: Boolean(agent.readOnly),\n maxSteps: agent.maxSteps ?? null,\n taskPlan: { enabled: isAgentTaskPlanEnabled(agent) },\n allowedTools: agent.allowedTools,\n tools,\n requiredFeatures: agent.requiredFeatures ?? [],\n acceptedMediaTypes: agent.acceptedMediaTypes ?? [],\n keywords: agent.keywords ?? [],\n suggestions: agent.suggestions ?? [],\n hasOutputSchema: Boolean(agent.output),\n }\n })\n\n return NextResponse.json({ agents, total: agents.length, aiConfigured })\n } catch (error) {\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,2BAA2B;AACpC,SAAS,wBAAwB,YAAY,yBAAyB;AACtE,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { isAgentTaskPlanEnabled, listAgents, loadAgentRegistry } from '../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport { toolRegistry } from '../../../lib/tool-registry'\nimport type { AiToolDefinition } from '../../../lib/types'\n\nconst logger = createLogger('ai_assistant')\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'List AI agents the caller can invoke',\n methods: {\n GET: {\n operationId: 'aiAssistantListAgents',\n summary: 'List registered AI agents, filtered by the caller\\'s features.',\n description:\n 'Returns `{ agents: [...] }` \u2014 the subset of agents from `ai-agents.generated.ts` that the ' +\n 'authenticated caller can invoke based on each agent\\'s `requiredFeatures`. Mirrors the ' +\n '`meta.list_agents` tool handler so backoffice pages (e.g. the playground) can render an ' +\n 'agent picker without going through the MCP tool transport.',\n responses: [\n {\n status: 200,\n description: 'Accessible agent summaries.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 500, description: 'Internal failure while loading the agent registry.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized', code: 'unauthenticated' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // No LLM provider configured (no API keys set). The launcher uses the\n // `aiConfigured` flag to show a setup prompt; explicit `<AiChat>` mounts\n // and playground pages still see the full registry so they can show their\n // own configuration prompts instead of silently disappearing.\n const aiConfigured = llmProviderRegistry.resolveFirstConfigured() != null\n\n await loadAgentRegistry()\n const all = listAgents()\n const accessible = all.filter((agent) =>\n hasRequiredFeatures(agent.requiredFeatures, acl.features, acl.isSuperAdmin, rbacService),\n )\n\n const agents = accessible.map((agent) => {\n const tools = agent.allowedTools.map((toolName) => {\n const tool = toolRegistry.getTool(toolName) as AiToolDefinition | undefined\n return {\n name: toolName,\n displayName: tool?.displayName ?? toolName,\n isMutation: Boolean(tool?.isMutation),\n registered: Boolean(tool),\n }\n })\n return {\n id: agent.id,\n moduleId: agent.moduleId,\n label: agent.label,\n description: agent.description,\n systemPrompt: agent.systemPrompt,\n executionMode: agent.executionMode ?? 'chat',\n defaultProvider: agent.defaultProvider ?? null,\n defaultModel: agent.defaultModel ?? null,\n defaultBaseUrl: agent.defaultBaseUrl ?? null,\n allowRuntimeModelOverride: agent.allowRuntimeModelOverride !== false,\n mutationPolicy: agent.mutationPolicy ?? 'read-only',\n readOnly: Boolean(agent.readOnly),\n maxSteps: agent.maxSteps ?? null,\n taskPlan: { enabled: isAgentTaskPlanEnabled(agent) },\n allowedTools: agent.allowedTools,\n tools,\n requiredFeatures: agent.requiredFeatures ?? [],\n acceptedMediaTypes: agent.acceptedMediaTypes ?? [],\n keywords: agent.keywords ?? [],\n suggestions: agent.suggestions ?? [],\n hasOutputSchema: Boolean(agent.output),\n }\n })\n\n return NextResponse.json({ agents, total: agents.length, aiConfigured })\n } catch (error) {\n logger.error('AI Agents \u2014 Failed to list agents', { err: error })\n return NextResponse.json(\n { error: 'Failed to list agents', code: 'internal_error' },\n { status: 500 },\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
1
2
|
import { NextResponse } from "next/server";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
@@ -25,6 +26,7 @@ import { AiTenantModelAllowlistRepository } from "../../../data/repositories/AiT
|
|
|
25
26
|
import { AiAgentRuntimeOverrideRepository } from "../../../data/repositories/AiAgentRuntimeOverrideRepository.js";
|
|
26
27
|
import { createConversationStorage } from "../../../lib/conversation-storage.js";
|
|
27
28
|
import { checkAiChatRateLimit } from "../../../lib/rate-limit.js";
|
|
29
|
+
const logger = createLogger("ai_assistant");
|
|
28
30
|
const MAX_MESSAGES = 100;
|
|
29
31
|
const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/;
|
|
30
32
|
const chatMessageSchema = z.object({
|
|
@@ -300,7 +302,7 @@ function persistAssistantOnStreamCompletion(input) {
|
|
|
300
302
|
);
|
|
301
303
|
}
|
|
302
304
|
} catch (error) {
|
|
303
|
-
|
|
305
|
+
logger.error("AI Chat Agent \u2014 Conversation persistence failure", { err: error });
|
|
304
306
|
} finally {
|
|
305
307
|
reader.releaseLock();
|
|
306
308
|
await writer.close().catch(() => void 0);
|
|
@@ -411,10 +413,7 @@ async function POST(req) {
|
|
|
411
413
|
} : null;
|
|
412
414
|
agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist) ? tenantAgentAllowlist : null;
|
|
413
415
|
} catch (snapshotError) {
|
|
414
|
-
|
|
415
|
-
"[AI Chat Agent] Tenant allowlist lookup failed; refusing to dispatch:",
|
|
416
|
-
snapshotError
|
|
417
|
-
);
|
|
416
|
+
logger.error("Tenant allowlist lookup failed; refusing to dispatch", { err: snapshotError });
|
|
418
417
|
return jsonError(
|
|
419
418
|
503,
|
|
420
419
|
"Tenant allowlist is temporarily unavailable. Try again shortly.",
|
|
@@ -514,7 +513,7 @@ async function POST(req) {
|
|
|
514
513
|
if (error instanceof Error && error.name === "AiChatConversationOrgNotFoundError") {
|
|
515
514
|
return jsonError(400, error.message, "organization_not_found");
|
|
516
515
|
}
|
|
517
|
-
|
|
516
|
+
logger.error("AI Chat Agent \u2014 Failed to persist user message", { err: error });
|
|
518
517
|
}
|
|
519
518
|
const response = await runAiAgentText({
|
|
520
519
|
agentId,
|
|
@@ -550,7 +549,7 @@ async function POST(req) {
|
|
|
550
549
|
if (error instanceof AgentPolicyError) {
|
|
551
550
|
return jsonError(statusForDenyCode(error.code), error.message, error.code);
|
|
552
551
|
}
|
|
553
|
-
|
|
552
|
+
logger.error("AI Chat Agent \u2014 Dispatch failure", { err: error });
|
|
554
553
|
return jsonError(
|
|
555
554
|
500,
|
|
556
555
|
error instanceof Error ? error.message : "Agent dispatch failed.",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ai_assistant/api/ai/chat/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport {\n runAiAgentText,\n resolveLoopBudgetPreset,\n type AiAgentLoopBudgetPreset,\n} from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\nimport { readBaseurlAllowlist, isBaseurlAllowlisted } from '../../../lib/baseurl-allowlist'\nimport {\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { createConversationStorage } from '../../../lib/conversation-storage'\nimport { checkAiChatRateLimit } from '../../../lib/rate-limit'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nconst MAX_MESSAGES = 100\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst chatMessageSchema = z.object({\n id: z.string().min(1).max(128).optional(),\n role: z.enum(['user', 'assistant', 'system']),\n content: z.string(),\n uiParts: z.array(z.unknown()).optional(),\n files: z\n .array(\n z\n .object({\n id: z.string().optional(),\n name: z.string().optional(),\n type: z.string().optional(),\n mimeType: z.string().optional(),\n size: z.number().optional(),\n })\n .passthrough(),\n )\n .optional(),\n})\n\nconst pageContextSchema = z\n .object({\n pageId: z.string().nullable().optional(),\n entityType: z.string().nullable().optional(),\n recordId: z.string().nullable().optional(),\n })\n .passthrough()\n\nconst chatRequestSchema = z.object({\n messages: z\n .array(chatMessageSchema)\n .min(1, 'messages must contain at least one message')\n .max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),\n attachmentIds: z.array(z.string()).optional(),\n debug: z.boolean().optional(),\n pageContext: pageContextSchema.optional(),\n /**\n * Stable per-conversation id (Phase 6.2). Wins over `conversationId` when\n * both are provided. The server echoes the resolved id on the SSE\n * `loop-finish` event so clients can persist it for the next turn.\n */\n sessionId: z.string().uuid().optional(),\n /**\n * @deprecated Use `sessionId` instead.\n */\n conversationId: z.string().min(1).max(128).optional(),\n})\n\nexport type AiChatRequest = z.infer<typeof chatRequestSchema>\n\nconst agentQuerySchema = z.object({\n agent: z\n .string()\n .regex(agentIdPattern, 'agent must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n /**\n * Per-request provider override. Must match a registered + configured\n * provider id. Validated against `llmProviderRegistry` at dispatch time.\n * Rejected when the agent has `allowRuntimeOverride: false`.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n provider: z.string().optional(),\n /**\n * Per-request model id override. Free-form string. Logged (not rejected)\n * when not in the provider's curated `defaultModels` catalog.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n model: z.string().optional(),\n /**\n * Per-request base URL override. Must parse as a URL and match\n * `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated host patterns). When the\n * env var is unset or empty, any non-empty value is rejected.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n baseUrl: z.string().optional(),\n /**\n * Named loop-budget preset. Maps to a fixed `loop.budget` triple:\n * tight \u2192 maxSteps: 3, maxWallClockMs: 10_000, maxTokens: 50_000\n * default \u2192 no override (agent default applies)\n * loose \u2192 maxSteps: 20, maxWallClockMs: 120_000, maxTokens: 500_000\n *\n * Rejected when the agent has `allowRuntimeOverride: false` or\n * `loop.allowRuntimeOverride: false`.\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n loopBudget: z.enum(['tight', 'default', 'loose']).optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI agent dispatcher',\n methods: {\n POST: {\n operationId: 'aiAssistantChatAgent',\n summary: 'Stream a chat turn for a registered AI agent',\n description:\n 'Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. ' +\n 'Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, ' +\n 'execution-mode compatibility, and attachment media-type policy. The streaming response ' +\n 'body uses an AI SDK-compatible `text/event-stream` transport. ' +\n 'Optional `?provider=`, `?model=`, and `?baseUrl=` query params let callers ' +\n 'override the resolved provider/model/base-URL for this turn (Phase 4a). ' +\n 'Provider must be registered and configured; baseUrl must match ' +\n '`AI_RUNTIME_BASEURL_ALLOWLIST` when set. Both are suppressed when the ' +\n 'agent declares `allowRuntimeOverride: false`.',\n query: agentQuerySchema,\n requestBody: {\n contentType: 'application/json',\n description: 'Chat turn payload. `messages` is required; `attachmentIds`, `debug`, and `pageContext` are optional.',\n schema: chatRequestSchema,\n },\n responses: [\n { status: 200, description: 'Streaming text/event-stream response compatible with AI SDK chat transports.', mediaType: 'text/event-stream' },\n ],\n errors: [\n {\n status: 400,\n description:\n 'Invalid query param, malformed payload, or message count above the cap. ' +\n 'Typed codes: `runtime_override_disabled` (agent has allowRuntimeOverride:false), ' +\n '`provider_unknown` (provider id not registered), ' +\n '`provider_not_configured` (provider registered but no API key in env), ' +\n '`baseurl_not_allowlisted` (baseUrl not in AI_RUNTIME_BASEURL_ALLOWLIST).',\n },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks agent-level or tool-level required features.' },\n { status: 404, description: 'Unknown agent id.' },\n { status: 409, description: 'Agent/tool/execution-mode policy violation.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nfunction statusForDenyCode(code: AgentPolicyDenyCode): number {\n switch (code) {\n case 'agent_unknown':\n return 404\n case 'agent_features_denied':\n case 'tool_features_denied':\n return 403\n case 'tool_not_whitelisted':\n case 'tool_unknown':\n case 'mutation_blocked_by_readonly':\n case 'mutation_blocked_by_policy':\n case 'execution_mode_not_supported':\n return 409\n case 'attachment_type_not_accepted':\n return 400\n default:\n return 409\n }\n}\n\nfunction extractDataPayload(eventBlock: string): string | null {\n const dataLines = eventBlock\n .split('\\n')\n .filter((line) => line.startsWith('data:'))\n .map((line) => (line.startsWith('data: ') ? line.slice(6) : line.slice(5)))\n if (dataLines.length === 0) return null\n return dataLines.join('\\n')\n}\n\nfunction extractUiPartsFromToolOutput(output: unknown): unknown[] {\n let parsed = output\n if (typeof output === 'string') {\n const trimmed = output.trim()\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return []\n try {\n parsed = JSON.parse(trimmed) as unknown\n } catch {\n return []\n }\n }\n if (!parsed || typeof parsed !== 'object') return []\n const value = parsed as Record<string, unknown>\n const parts: unknown[] = []\n if (value.status === 'pending-confirmation' || value.status === 'awaiting-confirmation') {\n const pendingActionId =\n typeof value.pendingActionId === 'string' && value.pendingActionId.length > 0\n ? value.pendingActionId\n : null\n if (pendingActionId) {\n parts.push({\n componentId: 'mutation-preview-card',\n pendingActionId,\n payload: {\n pendingActionId,\n expiresAt: typeof value.expiresAt === 'string' ? value.expiresAt : undefined,\n agentId:\n typeof value.agentId === 'string'\n ? value.agentId\n : typeof value.agent === 'string'\n ? value.agent\n : undefined,\n toolName: typeof value.toolName === 'string' ? value.toolName : undefined,\n },\n })\n }\n }\n if (value.uiPart && typeof value.uiPart === 'object') parts.push(value.uiPart)\n if (Array.isArray(value.uiParts)) parts.push(...value.uiParts)\n return parts\n}\n\nfunction extractAssistantSnapshot(\n raw: string,\n contentType: string | null,\n): { content: string; uiParts: unknown[] } {\n if (!contentType?.includes('event-stream')) {\n return { content: raw, uiParts: [] }\n }\n let content = ''\n const uiParts: unknown[] = []\n for (const block of raw.split('\\n\\n')) {\n const data = extractDataPayload(block)\n if (!data || data === '[DONE]') continue\n try {\n const parsed = JSON.parse(data) as Record<string, unknown>\n if (parsed.type === 'text-delta' && typeof parsed.delta === 'string') {\n content += parsed.delta\n } else if (parsed.type === 'text' && typeof parsed.content === 'string') {\n content += parsed.content\n } else if (parsed.type === 'tool-output-available') {\n uiParts.push(...extractUiPartsFromToolOutput(parsed.output))\n }\n } catch {\n // Ignore SSE comments and malformed provider chunks.\n }\n }\n return { content, uiParts }\n}\n\nasync function persistChatTurnStart(input: {\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n userId: string\n agentId: string\n conversationId: string | null\n pageContext?: Record<string, unknown>\n messages: AiChatRequest['messages']\n attachmentIds?: string[]\n}): Promise<{ conversationId: string; userClientMessageId: string | null } | null> {\n if (!input.tenantId || !input.conversationId) return null\n const repo = createConversationStorage(input.container)\n const ctx = {\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n }\n await repo.createOrGet(\n {\n conversationId: input.conversationId,\n agentId: input.agentId,\n pageContext: input.pageContext ?? null,\n },\n ctx,\n )\n const userMessage = [...input.messages].reverse().find((message) => message.role === 'user')\n if (!userMessage) return { conversationId: input.conversationId, userClientMessageId: null }\n await repo.appendMessage(\n input.conversationId,\n {\n clientMessageId: userMessage.id,\n role: 'user',\n content: userMessage.content,\n uiParts: userMessage.uiParts,\n attachmentIds: input.attachmentIds,\n files: userMessage.files?.map((file, index) => {\n const id = file.id ?? input.attachmentIds?.[index]\n const mimeType = file.mimeType ?? file.type\n return {\n ...(id ? { id } : {}),\n ...(file.name ? { name: file.name } : {}),\n ...(mimeType ? { mimeType } : {}),\n ...(typeof file.size === 'number' ? { size: file.size } : {}),\n }\n }),\n },\n ctx,\n )\n return {\n conversationId: input.conversationId,\n userClientMessageId: userMessage.id ?? null,\n }\n}\n\nfunction persistAssistantOnStreamCompletion(input: {\n response: Response\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n userId: string\n conversationId: string\n userClientMessageId: string | null\n}): Response {\n if (!input.response.body || !input.tenantId) return input.response\n const tenantId = input.tenantId\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n const writer = writable.getWriter()\n const decoder = new TextDecoder()\n const contentType = input.response.headers.get('content-type')\n\n async function pump(): Promise<void> {\n const reader = input.response.body!.getReader()\n let raw = ''\n try {\n for (;;) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n raw += decoder.decode(value, { stream: true })\n await writer.write(value)\n }\n raw += decoder.decode()\n const assistant = extractAssistantSnapshot(raw, contentType)\n if (assistant.content.trim() || assistant.uiParts.length > 0) {\n const repo = createConversationStorage(input.container)\n await repo.appendMessage(\n input.conversationId,\n {\n clientMessageId: input.userClientMessageId\n ? `${input.userClientMessageId}:assistant`\n : undefined,\n role: 'assistant',\n content: assistant.content,\n uiParts: assistant.uiParts,\n },\n {\n tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n },\n )\n }\n } catch (error) {\n console.error('[AI Chat Agent] Conversation persistence failure:', error)\n } finally {\n reader.releaseLock()\n await writer.close().catch(() => undefined)\n }\n }\n\n void pump()\n return new Response(readable, {\n status: input.response.status,\n statusText: input.response.statusText,\n headers: input.response.headers,\n })\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const requestUrl = new URL(req.url)\n const queryResult = agentQuerySchema.safeParse({\n agent: requestUrl.searchParams.get('agent') ?? undefined,\n provider: requestUrl.searchParams.get('provider') ?? undefined,\n model: requestUrl.searchParams.get('model') ?? undefined,\n baseUrl: requestUrl.searchParams.get('baseUrl') ?? undefined,\n loopBudget: requestUrl.searchParams.get('loopBudget') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid or missing \"agent\" query parameter.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n const agentId = queryResult.data.agent\n const rawProvider = queryResult.data.provider\n const rawModel = queryResult.data.model\n const rawBaseUrl = queryResult.data.baseUrl\n const rawLoopBudget = queryResult.data.loopBudget as AiAgentLoopBudgetPreset | undefined\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = chatRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n\n const rateLimited = await checkAiChatRateLimit({\n req,\n container,\n userId: auth.sub,\n tenantId: auth.tenantId,\n })\n if (rateLimited) return rateLimited\n\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const decision = checkAgentPolicy({\n agentId,\n authContext: {\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n requestedExecutionMode: 'chat',\n // TODO(step-3.7): resolve attachmentIds -> media types via attachment-bridge\n // once the attachment-to-model conversion bridge lands. Until then the\n // policy gate skips attachment-type validation because media types are\n // not known at dispatch time.\n attachmentMediaTypes: undefined,\n })\n\n if (!decision.ok) {\n return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)\n }\n\n const agentDef = decision.agent\n\n // --- Phase 4a: validate runtime override query params ---\n const hasRuntimeOverride =\n (rawProvider && rawProvider.trim().length > 0) ||\n (rawModel && rawModel.trim().length > 0) ||\n (rawBaseUrl && rawBaseUrl.trim().length > 0) ||\n (rawLoopBudget !== undefined && rawLoopBudget !== 'default')\n\n // `allowRuntimeOverride` is the canonical flag (renamed from\n // `allowRuntimeModelOverride` in Phase 4 of this spec). Both are checked\n // here to cover agents declared before the rename lands; the deprecated\n // alias has lower priority.\n const runtimeOverrideAllowed =\n agentDef.allowRuntimeOverride !== false &&\n agentDef.allowRuntimeModelOverride !== false\n\n if (hasRuntimeOverride && !runtimeOverrideAllowed) {\n return jsonError(\n 400,\n `Agent \"${agentId}\" has runtime override disabled (allowRuntimeOverride: false).`,\n 'runtime_override_disabled',\n )\n }\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n let agentRuntimeOverrideAllowlist: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = container.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em)\n const agentRuntimeOverrideRow = await runtimeOverrideRepo.getExact({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n const tenantAgentAllowlist = agentRuntimeOverrideRow\n ? {\n allowedProviders: agentRuntimeOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentRuntimeOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist)\n ? tenantAgentAllowlist\n : null\n } catch (snapshotError) {\n // Fail closed: refuse to dispatch if we cannot confirm the tenant allowlist.\n // Silently falling back to env-only would widen the effective allowlist when\n // the DB is unavailable, which is the opposite of what an admin intends.\n console.error(\n '[AI Chat Agent] Tenant allowlist lookup failed; refusing to dispatch:',\n snapshotError,\n )\n return jsonError(\n 503,\n 'Tenant allowlist is temporarily unavailable. Try again shortly.',\n 'tenant_allowlist_unavailable',\n )\n }\n }\n const knownProviderIds = llmProviderRegistry.list().map((p) => p.id)\n const baseEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownProviderIds,\n tenantAllowlistSnapshot,\n )\n const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(\n process.env as Record<string, string | undefined>,\n agentId,\n knownProviderIds,\n )\n const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIds,\n envAgentAllowlist,\n ),\n knownProviderIds,\n agentRuntimeOverrideAllowlist,\n )\n\n const normalizedProvider = rawProvider && rawProvider.trim().length > 0\n ? canonicalProviderId(rawProvider.trim(), llmProviderRegistry.list().map((p) => p.id))\n : null\n\n if (rawProvider && rawProvider.trim().length > 0) {\n const providerEntry = normalizedProvider ? llmProviderRegistry.get(normalizedProvider) : null\n if (!providerEntry) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not registered. Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(', ')}.`,\n 'provider_unknown',\n )\n }\n if (!providerEntry.isConfigured()) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is registered but not configured in this environment (missing API key).`,\n 'provider_not_configured',\n )\n }\n if (!isProviderAllowedInEffective(effectiveAllowlist, normalizedProvider!)) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not in ${source}.`,\n 'provider_not_allowlisted',\n )\n }\n if (\n rawModel\n && rawModel.trim().length > 0\n && !isModelAllowedForProviderInEffective(\n effectiveAllowlist,\n normalizedProvider!,\n rawModel.trim(),\n )\n ) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${normalizedProvider}\"`\n : modelAllowlistEnvVarName(normalizedProvider!)\n return jsonError(\n 400,\n `Model \"${rawModel}\" is not in ${source}.`,\n 'model_not_allowlisted',\n )\n }\n }\n\n if (rawBaseUrl && rawBaseUrl.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(rawBaseUrl.trim(), allowlist)) {\n return jsonError(\n 400,\n `baseUrl \"${rawBaseUrl}\" is not in the AI_RUNTIME_BASEURL_ALLOWLIST. Set that env var to a comma-separated list of allowed host patterns to enable per-request baseUrl overrides.`,\n 'baseurl_not_allowlisted',\n )\n }\n }\n // --- end Phase 4a + Phase 4 validation ---\n\n const requestOverride =\n hasRuntimeOverride\n ? {\n providerId: normalizedProvider,\n modelId: rawModel && rawModel.trim().length > 0 ? rawModel.trim() : null,\n baseURL: rawBaseUrl && rawBaseUrl.trim().length > 0 ? rawBaseUrl.trim() : null,\n }\n : undefined\n\n // Resolve the loopBudget preset to a loop config override (Phase 4).\n const loopFromPreset =\n rawLoopBudget !== undefined && rawLoopBudget !== 'default'\n ? resolveLoopBudgetPreset(rawLoopBudget)\n : undefined\n\n const effectiveConversationId = bodyResult.data.sessionId ?? bodyResult.data.conversationId ?? null\n let persistedTurn:\n | { conversationId: string; userClientMessageId: string | null }\n | null = null\n try {\n persistedTurn = await persistChatTurnStart({\n container,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n agentId,\n conversationId: effectiveConversationId,\n pageContext: bodyResult.data.pageContext,\n messages: bodyResult.data.messages,\n attachmentIds: bodyResult.data.attachmentIds,\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationOrgNotFoundError') {\n return jsonError(400, error.message, 'organization_not_found')\n }\n console.error('[AI Chat Agent] Failed to persist user message:', error)\n }\n\n const response = await runAiAgentText({\n agentId,\n messages: bodyResult.data.messages as unknown as UIMessage[],\n attachmentIds: bodyResult.data.attachmentIds,\n pageContext: bodyResult.data.pageContext,\n debug: bodyResult.data.debug,\n sessionId: bodyResult.data.sessionId ?? null,\n conversationId: bodyResult.data.conversationId ?? null,\n authContext: {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n container,\n requestOverride,\n loop: loopFromPreset,\n emitLoopTrace: true,\n })\n if (!persistedTurn) return response\n return persistAssistantOnStreamCompletion({\n response,\n container,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: persistedTurn.conversationId,\n userClientMessageId: persistedTurn.userClientMessageId,\n })\n } catch (error) {\n if (error instanceof AgentPolicyError) {\n return jsonError(statusForDenyCode(error.code), error.message, error.code)\n }\n console.error('[AI Chat Agent] Dispatch failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Agent dispatch failed.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport {\n runAiAgentText,\n resolveLoopBudgetPreset,\n type AiAgentLoopBudgetPreset,\n} from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\nimport { readBaseurlAllowlist, isBaseurlAllowlisted } from '../../../lib/baseurl-allowlist'\nimport {\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { createConversationStorage } from '../../../lib/conversation-storage'\nimport { checkAiChatRateLimit } from '../../../lib/rate-limit'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nconst logger = createLogger('ai_assistant')\n\nconst MAX_MESSAGES = 100\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst chatMessageSchema = z.object({\n id: z.string().min(1).max(128).optional(),\n role: z.enum(['user', 'assistant', 'system']),\n content: z.string(),\n uiParts: z.array(z.unknown()).optional(),\n files: z\n .array(\n z\n .object({\n id: z.string().optional(),\n name: z.string().optional(),\n type: z.string().optional(),\n mimeType: z.string().optional(),\n size: z.number().optional(),\n })\n .passthrough(),\n )\n .optional(),\n})\n\nconst pageContextSchema = z\n .object({\n pageId: z.string().nullable().optional(),\n entityType: z.string().nullable().optional(),\n recordId: z.string().nullable().optional(),\n })\n .passthrough()\n\nconst chatRequestSchema = z.object({\n messages: z\n .array(chatMessageSchema)\n .min(1, 'messages must contain at least one message')\n .max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),\n attachmentIds: z.array(z.string()).optional(),\n debug: z.boolean().optional(),\n pageContext: pageContextSchema.optional(),\n /**\n * Stable per-conversation id (Phase 6.2). Wins over `conversationId` when\n * both are provided. The server echoes the resolved id on the SSE\n * `loop-finish` event so clients can persist it for the next turn.\n */\n sessionId: z.string().uuid().optional(),\n /**\n * @deprecated Use `sessionId` instead.\n */\n conversationId: z.string().min(1).max(128).optional(),\n})\n\nexport type AiChatRequest = z.infer<typeof chatRequestSchema>\n\nconst agentQuerySchema = z.object({\n agent: z\n .string()\n .regex(agentIdPattern, 'agent must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n /**\n * Per-request provider override. Must match a registered + configured\n * provider id. Validated against `llmProviderRegistry` at dispatch time.\n * Rejected when the agent has `allowRuntimeOverride: false`.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n provider: z.string().optional(),\n /**\n * Per-request model id override. Free-form string. Logged (not rejected)\n * when not in the provider's curated `defaultModels` catalog.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n model: z.string().optional(),\n /**\n * Per-request base URL override. Must parse as a URL and match\n * `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated host patterns). When the\n * env var is unset or empty, any non-empty value is rejected.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n baseUrl: z.string().optional(),\n /**\n * Named loop-budget preset. Maps to a fixed `loop.budget` triple:\n * tight \u2192 maxSteps: 3, maxWallClockMs: 10_000, maxTokens: 50_000\n * default \u2192 no override (agent default applies)\n * loose \u2192 maxSteps: 20, maxWallClockMs: 120_000, maxTokens: 500_000\n *\n * Rejected when the agent has `allowRuntimeOverride: false` or\n * `loop.allowRuntimeOverride: false`.\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n loopBudget: z.enum(['tight', 'default', 'loose']).optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI agent dispatcher',\n methods: {\n POST: {\n operationId: 'aiAssistantChatAgent',\n summary: 'Stream a chat turn for a registered AI agent',\n description:\n 'Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. ' +\n 'Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, ' +\n 'execution-mode compatibility, and attachment media-type policy. The streaming response ' +\n 'body uses an AI SDK-compatible `text/event-stream` transport. ' +\n 'Optional `?provider=`, `?model=`, and `?baseUrl=` query params let callers ' +\n 'override the resolved provider/model/base-URL for this turn (Phase 4a). ' +\n 'Provider must be registered and configured; baseUrl must match ' +\n '`AI_RUNTIME_BASEURL_ALLOWLIST` when set. Both are suppressed when the ' +\n 'agent declares `allowRuntimeOverride: false`.',\n query: agentQuerySchema,\n requestBody: {\n contentType: 'application/json',\n description: 'Chat turn payload. `messages` is required; `attachmentIds`, `debug`, and `pageContext` are optional.',\n schema: chatRequestSchema,\n },\n responses: [\n { status: 200, description: 'Streaming text/event-stream response compatible with AI SDK chat transports.', mediaType: 'text/event-stream' },\n ],\n errors: [\n {\n status: 400,\n description:\n 'Invalid query param, malformed payload, or message count above the cap. ' +\n 'Typed codes: `runtime_override_disabled` (agent has allowRuntimeOverride:false), ' +\n '`provider_unknown` (provider id not registered), ' +\n '`provider_not_configured` (provider registered but no API key in env), ' +\n '`baseurl_not_allowlisted` (baseUrl not in AI_RUNTIME_BASEURL_ALLOWLIST).',\n },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks agent-level or tool-level required features.' },\n { status: 404, description: 'Unknown agent id.' },\n { status: 409, description: 'Agent/tool/execution-mode policy violation.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nfunction statusForDenyCode(code: AgentPolicyDenyCode): number {\n switch (code) {\n case 'agent_unknown':\n return 404\n case 'agent_features_denied':\n case 'tool_features_denied':\n return 403\n case 'tool_not_whitelisted':\n case 'tool_unknown':\n case 'mutation_blocked_by_readonly':\n case 'mutation_blocked_by_policy':\n case 'execution_mode_not_supported':\n return 409\n case 'attachment_type_not_accepted':\n return 400\n default:\n return 409\n }\n}\n\nfunction extractDataPayload(eventBlock: string): string | null {\n const dataLines = eventBlock\n .split('\\n')\n .filter((line) => line.startsWith('data:'))\n .map((line) => (line.startsWith('data: ') ? line.slice(6) : line.slice(5)))\n if (dataLines.length === 0) return null\n return dataLines.join('\\n')\n}\n\nfunction extractUiPartsFromToolOutput(output: unknown): unknown[] {\n let parsed = output\n if (typeof output === 'string') {\n const trimmed = output.trim()\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return []\n try {\n parsed = JSON.parse(trimmed) as unknown\n } catch {\n return []\n }\n }\n if (!parsed || typeof parsed !== 'object') return []\n const value = parsed as Record<string, unknown>\n const parts: unknown[] = []\n if (value.status === 'pending-confirmation' || value.status === 'awaiting-confirmation') {\n const pendingActionId =\n typeof value.pendingActionId === 'string' && value.pendingActionId.length > 0\n ? value.pendingActionId\n : null\n if (pendingActionId) {\n parts.push({\n componentId: 'mutation-preview-card',\n pendingActionId,\n payload: {\n pendingActionId,\n expiresAt: typeof value.expiresAt === 'string' ? value.expiresAt : undefined,\n agentId:\n typeof value.agentId === 'string'\n ? value.agentId\n : typeof value.agent === 'string'\n ? value.agent\n : undefined,\n toolName: typeof value.toolName === 'string' ? value.toolName : undefined,\n },\n })\n }\n }\n if (value.uiPart && typeof value.uiPart === 'object') parts.push(value.uiPart)\n if (Array.isArray(value.uiParts)) parts.push(...value.uiParts)\n return parts\n}\n\nfunction extractAssistantSnapshot(\n raw: string,\n contentType: string | null,\n): { content: string; uiParts: unknown[] } {\n if (!contentType?.includes('event-stream')) {\n return { content: raw, uiParts: [] }\n }\n let content = ''\n const uiParts: unknown[] = []\n for (const block of raw.split('\\n\\n')) {\n const data = extractDataPayload(block)\n if (!data || data === '[DONE]') continue\n try {\n const parsed = JSON.parse(data) as Record<string, unknown>\n if (parsed.type === 'text-delta' && typeof parsed.delta === 'string') {\n content += parsed.delta\n } else if (parsed.type === 'text' && typeof parsed.content === 'string') {\n content += parsed.content\n } else if (parsed.type === 'tool-output-available') {\n uiParts.push(...extractUiPartsFromToolOutput(parsed.output))\n }\n } catch {\n // Ignore SSE comments and malformed provider chunks.\n }\n }\n return { content, uiParts }\n}\n\nasync function persistChatTurnStart(input: {\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n userId: string\n agentId: string\n conversationId: string | null\n pageContext?: Record<string, unknown>\n messages: AiChatRequest['messages']\n attachmentIds?: string[]\n}): Promise<{ conversationId: string; userClientMessageId: string | null } | null> {\n if (!input.tenantId || !input.conversationId) return null\n const repo = createConversationStorage(input.container)\n const ctx = {\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n }\n await repo.createOrGet(\n {\n conversationId: input.conversationId,\n agentId: input.agentId,\n pageContext: input.pageContext ?? null,\n },\n ctx,\n )\n const userMessage = [...input.messages].reverse().find((message) => message.role === 'user')\n if (!userMessage) return { conversationId: input.conversationId, userClientMessageId: null }\n await repo.appendMessage(\n input.conversationId,\n {\n clientMessageId: userMessage.id,\n role: 'user',\n content: userMessage.content,\n uiParts: userMessage.uiParts,\n attachmentIds: input.attachmentIds,\n files: userMessage.files?.map((file, index) => {\n const id = file.id ?? input.attachmentIds?.[index]\n const mimeType = file.mimeType ?? file.type\n return {\n ...(id ? { id } : {}),\n ...(file.name ? { name: file.name } : {}),\n ...(mimeType ? { mimeType } : {}),\n ...(typeof file.size === 'number' ? { size: file.size } : {}),\n }\n }),\n },\n ctx,\n )\n return {\n conversationId: input.conversationId,\n userClientMessageId: userMessage.id ?? null,\n }\n}\n\nfunction persistAssistantOnStreamCompletion(input: {\n response: Response\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n userId: string\n conversationId: string\n userClientMessageId: string | null\n}): Response {\n if (!input.response.body || !input.tenantId) return input.response\n const tenantId = input.tenantId\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n const writer = writable.getWriter()\n const decoder = new TextDecoder()\n const contentType = input.response.headers.get('content-type')\n\n async function pump(): Promise<void> {\n const reader = input.response.body!.getReader()\n let raw = ''\n try {\n for (;;) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n raw += decoder.decode(value, { stream: true })\n await writer.write(value)\n }\n raw += decoder.decode()\n const assistant = extractAssistantSnapshot(raw, contentType)\n if (assistant.content.trim() || assistant.uiParts.length > 0) {\n const repo = createConversationStorage(input.container)\n await repo.appendMessage(\n input.conversationId,\n {\n clientMessageId: input.userClientMessageId\n ? `${input.userClientMessageId}:assistant`\n : undefined,\n role: 'assistant',\n content: assistant.content,\n uiParts: assistant.uiParts,\n },\n {\n tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n },\n )\n }\n } catch (error) {\n logger.error('AI Chat Agent \u2014 Conversation persistence failure', { err: error })\n } finally {\n reader.releaseLock()\n await writer.close().catch(() => undefined)\n }\n }\n\n void pump()\n return new Response(readable, {\n status: input.response.status,\n statusText: input.response.statusText,\n headers: input.response.headers,\n })\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const requestUrl = new URL(req.url)\n const queryResult = agentQuerySchema.safeParse({\n agent: requestUrl.searchParams.get('agent') ?? undefined,\n provider: requestUrl.searchParams.get('provider') ?? undefined,\n model: requestUrl.searchParams.get('model') ?? undefined,\n baseUrl: requestUrl.searchParams.get('baseUrl') ?? undefined,\n loopBudget: requestUrl.searchParams.get('loopBudget') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid or missing \"agent\" query parameter.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n const agentId = queryResult.data.agent\n const rawProvider = queryResult.data.provider\n const rawModel = queryResult.data.model\n const rawBaseUrl = queryResult.data.baseUrl\n const rawLoopBudget = queryResult.data.loopBudget as AiAgentLoopBudgetPreset | undefined\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = chatRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n\n const rateLimited = await checkAiChatRateLimit({\n req,\n container,\n userId: auth.sub,\n tenantId: auth.tenantId,\n })\n if (rateLimited) return rateLimited\n\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const decision = checkAgentPolicy({\n agentId,\n authContext: {\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n requestedExecutionMode: 'chat',\n // TODO(step-3.7): resolve attachmentIds -> media types via attachment-bridge\n // once the attachment-to-model conversion bridge lands. Until then the\n // policy gate skips attachment-type validation because media types are\n // not known at dispatch time.\n attachmentMediaTypes: undefined,\n })\n\n if (!decision.ok) {\n return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)\n }\n\n const agentDef = decision.agent\n\n // --- Phase 4a: validate runtime override query params ---\n const hasRuntimeOverride =\n (rawProvider && rawProvider.trim().length > 0) ||\n (rawModel && rawModel.trim().length > 0) ||\n (rawBaseUrl && rawBaseUrl.trim().length > 0) ||\n (rawLoopBudget !== undefined && rawLoopBudget !== 'default')\n\n // `allowRuntimeOverride` is the canonical flag (renamed from\n // `allowRuntimeModelOverride` in Phase 4 of this spec). Both are checked\n // here to cover agents declared before the rename lands; the deprecated\n // alias has lower priority.\n const runtimeOverrideAllowed =\n agentDef.allowRuntimeOverride !== false &&\n agentDef.allowRuntimeModelOverride !== false\n\n if (hasRuntimeOverride && !runtimeOverrideAllowed) {\n return jsonError(\n 400,\n `Agent \"${agentId}\" has runtime override disabled (allowRuntimeOverride: false).`,\n 'runtime_override_disabled',\n )\n }\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n let agentRuntimeOverrideAllowlist: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = container.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em)\n const agentRuntimeOverrideRow = await runtimeOverrideRepo.getExact({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n const tenantAgentAllowlist = agentRuntimeOverrideRow\n ? {\n allowedProviders: agentRuntimeOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentRuntimeOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist)\n ? tenantAgentAllowlist\n : null\n } catch (snapshotError) {\n // Fail closed: refuse to dispatch if we cannot confirm the tenant allowlist.\n // Silently falling back to env-only would widen the effective allowlist when\n // the DB is unavailable, which is the opposite of what an admin intends.\n logger.error('Tenant allowlist lookup failed; refusing to dispatch', { err: snapshotError })\n return jsonError(\n 503,\n 'Tenant allowlist is temporarily unavailable. Try again shortly.',\n 'tenant_allowlist_unavailable',\n )\n }\n }\n const knownProviderIds = llmProviderRegistry.list().map((p) => p.id)\n const baseEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownProviderIds,\n tenantAllowlistSnapshot,\n )\n const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(\n process.env as Record<string, string | undefined>,\n agentId,\n knownProviderIds,\n )\n const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIds,\n envAgentAllowlist,\n ),\n knownProviderIds,\n agentRuntimeOverrideAllowlist,\n )\n\n const normalizedProvider = rawProvider && rawProvider.trim().length > 0\n ? canonicalProviderId(rawProvider.trim(), llmProviderRegistry.list().map((p) => p.id))\n : null\n\n if (rawProvider && rawProvider.trim().length > 0) {\n const providerEntry = normalizedProvider ? llmProviderRegistry.get(normalizedProvider) : null\n if (!providerEntry) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not registered. Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(', ')}.`,\n 'provider_unknown',\n )\n }\n if (!providerEntry.isConfigured()) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is registered but not configured in this environment (missing API key).`,\n 'provider_not_configured',\n )\n }\n if (!isProviderAllowedInEffective(effectiveAllowlist, normalizedProvider!)) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not in ${source}.`,\n 'provider_not_allowlisted',\n )\n }\n if (\n rawModel\n && rawModel.trim().length > 0\n && !isModelAllowedForProviderInEffective(\n effectiveAllowlist,\n normalizedProvider!,\n rawModel.trim(),\n )\n ) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${normalizedProvider}\"`\n : modelAllowlistEnvVarName(normalizedProvider!)\n return jsonError(\n 400,\n `Model \"${rawModel}\" is not in ${source}.`,\n 'model_not_allowlisted',\n )\n }\n }\n\n if (rawBaseUrl && rawBaseUrl.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(rawBaseUrl.trim(), allowlist)) {\n return jsonError(\n 400,\n `baseUrl \"${rawBaseUrl}\" is not in the AI_RUNTIME_BASEURL_ALLOWLIST. Set that env var to a comma-separated list of allowed host patterns to enable per-request baseUrl overrides.`,\n 'baseurl_not_allowlisted',\n )\n }\n }\n // --- end Phase 4a + Phase 4 validation ---\n\n const requestOverride =\n hasRuntimeOverride\n ? {\n providerId: normalizedProvider,\n modelId: rawModel && rawModel.trim().length > 0 ? rawModel.trim() : null,\n baseURL: rawBaseUrl && rawBaseUrl.trim().length > 0 ? rawBaseUrl.trim() : null,\n }\n : undefined\n\n // Resolve the loopBudget preset to a loop config override (Phase 4).\n const loopFromPreset =\n rawLoopBudget !== undefined && rawLoopBudget !== 'default'\n ? resolveLoopBudgetPreset(rawLoopBudget)\n : undefined\n\n const effectiveConversationId = bodyResult.data.sessionId ?? bodyResult.data.conversationId ?? null\n let persistedTurn:\n | { conversationId: string; userClientMessageId: string | null }\n | null = null\n try {\n persistedTurn = await persistChatTurnStart({\n container,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n agentId,\n conversationId: effectiveConversationId,\n pageContext: bodyResult.data.pageContext,\n messages: bodyResult.data.messages,\n attachmentIds: bodyResult.data.attachmentIds,\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationOrgNotFoundError') {\n return jsonError(400, error.message, 'organization_not_found')\n }\n logger.error('AI Chat Agent \u2014 Failed to persist user message', { err: error })\n }\n\n const response = await runAiAgentText({\n agentId,\n messages: bodyResult.data.messages as unknown as UIMessage[],\n attachmentIds: bodyResult.data.attachmentIds,\n pageContext: bodyResult.data.pageContext,\n debug: bodyResult.data.debug,\n sessionId: bodyResult.data.sessionId ?? null,\n conversationId: bodyResult.data.conversationId ?? null,\n authContext: {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n container,\n requestOverride,\n loop: loopFromPreset,\n emitLoopTrace: true,\n })\n if (!persistedTurn) return response\n return persistAssistantOnStreamCompletion({\n response,\n container,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: persistedTurn.conversationId,\n userClientMessageId: persistedTurn.userClientMessageId,\n })\n } catch (error) {\n if (error instanceof AgentPolicyError) {\n return jsonError(statusForDenyCode(error.code), error.message, error.code)\n }\n logger.error('AI Chat Agent \u2014 Dispatch failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Agent dispatch failed.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
1
2
|
import { NextResponse } from "next/server";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
serializeAiChatConversation,
|
|
13
14
|
serializeAiChatMessage
|
|
14
15
|
} from "../../../../lib/conversation-storage.js";
|
|
16
|
+
const logger = createLogger("ai_assistant");
|
|
15
17
|
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
16
18
|
const MANAGE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.manage";
|
|
17
19
|
const conversationIdParamSchema = z.object({
|
|
@@ -174,7 +176,7 @@ async function GET(req, context) {
|
|
|
174
176
|
nextCursor: transcript.nextCursor
|
|
175
177
|
});
|
|
176
178
|
} catch (error) {
|
|
177
|
-
|
|
179
|
+
logger.error("AI Conversation GET \u2014 Failure", { err: error });
|
|
178
180
|
return jsonError(
|
|
179
181
|
500,
|
|
180
182
|
error instanceof Error ? error.message : "Failed to load conversation.",
|
|
@@ -226,7 +228,7 @@ async function PATCH(req, context) {
|
|
|
226
228
|
if (error instanceof Error && error.name === "AiChatConversationAccessError") {
|
|
227
229
|
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
228
230
|
}
|
|
229
|
-
|
|
231
|
+
logger.error("AI Conversation PATCH \u2014 Failure", { err: error });
|
|
230
232
|
return jsonError(
|
|
231
233
|
500,
|
|
232
234
|
error instanceof Error ? error.message : "Failed to update conversation.",
|
|
@@ -262,7 +264,7 @@ async function DELETE(req, context) {
|
|
|
262
264
|
if (error instanceof Error && error.name === "AiChatConversationAccessError") {
|
|
263
265
|
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
264
266
|
}
|
|
265
|
-
|
|
267
|
+
logger.error("AI Conversation DELETE \u2014 Failure", { err: error });
|
|
266
268
|
return jsonError(
|
|
267
269
|
500,
|
|
268
270
|
error instanceof Error ? error.message : "Failed to delete conversation.",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n aiChatConversationTranscriptQuerySchema,\n aiChatConversationUpdateSchema,\n} from '../../../../data/validators'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n serializeAiChatMessage,\n} from '../../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Per-conversation AI chat operations',\n methods: {\n GET: {\n operationId: 'aiAssistantGetConversation',\n summary: 'Fetch a conversation summary and recent transcript.',\n description:\n 'Returns `{ conversation, messages, nextCursor }` for the supplied `conversationId`. ' +\n 'View-only callers can load only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can load conversations across users in the same ' +\n 'tenant/organization. Messages are ordered ascending by `createdAt`. The `before` cursor ' +\n 'returns the next older page when paging back through long transcripts.',\n responses: [\n {\n status: 200,\n description: 'Conversation transcript page for the authenticated owner.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path or query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n PATCH: {\n operationId: 'aiAssistantUpdateConversation',\n summary: 'Update an existing conversation.',\n description:\n 'Accepts a partial body containing any of `title`, `status`, `pageContext`. Setting ' +\n '`status` to `closed` archives the conversation while keeping its transcript intact. ' +\n 'View-only callers can update only their own conversations; conversation managers can ' +\n 'update conversations in the same tenant/organization.',\n responses: [\n {\n status: 200,\n description: 'Updated conversation summary.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n DELETE: {\n operationId: 'aiAssistantDeleteConversation',\n summary: 'Soft-delete a conversation and its messages.',\n description:\n 'View-only callers can delete only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can delete conversations in the same tenant/organization. ' +\n 'Marks the conversation row and every undeleted message row with a `deleted_at` timestamp ' +\n 'in one transaction. The transcript remains in the database for audit/restore until a future ' +\n 'retention worker hard-deletes it.',\n responses: [\n {\n status: 200,\n description: 'Soft-delete acknowledgment.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n PATCH: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n const canManageConversations = hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n const url = new URL(req.url)\n const queryResult = aiChatConversationTranscriptQuerySchema.safeParse({\n limit: url.searchParams.get('limit') ?? undefined,\n before: url.searchParams.get('before') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const transcript = await repo.getTranscript(\n callerCtx.conversationId,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n limit: queryResult.data.limit,\n before: queryResult.data.before ?? null,\n },\n )\n if (!transcript) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n const participantCount = await repo.getParticipantCount(\n callerCtx.tenantId,\n callerCtx.organizationId,\n callerCtx.conversationId,\n )\n return NextResponse.json({\n conversation: serializeAiChatConversation(transcript.conversation, {\n callerUserId: callerCtx.userId,\n participantCount,\n }),\n messages: transcript.messages.map(serializeAiChatMessage),\n nextCursor: transcript.nextCursor,\n })\n } catch (error) {\n console.error('[AI Conversation GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function PATCH(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n const parseResult = aiChatConversationUpdateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation patch.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const row = await repo.update(\n callerCtx.conversationId,\n parseResult.data,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n return NextResponse.json(serializeAiChatConversation(row))\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation PATCH] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to update conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n await repo.softDelete(callerCtx.conversationId, {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation DELETE] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to delete conversation.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {\n aiChatConversationTranscriptQuerySchema,\n aiChatConversationUpdateSchema,\n} from '../../../../data/validators'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n serializeAiChatMessage,\n} from '../../../../lib/conversation-storage'\n\nconst logger = createLogger('ai_assistant')\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Per-conversation AI chat operations',\n methods: {\n GET: {\n operationId: 'aiAssistantGetConversation',\n summary: 'Fetch a conversation summary and recent transcript.',\n description:\n 'Returns `{ conversation, messages, nextCursor }` for the supplied `conversationId`. ' +\n 'View-only callers can load only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can load conversations across users in the same ' +\n 'tenant/organization. Messages are ordered ascending by `createdAt`. The `before` cursor ' +\n 'returns the next older page when paging back through long transcripts.',\n responses: [\n {\n status: 200,\n description: 'Conversation transcript page for the authenticated owner.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path or query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n PATCH: {\n operationId: 'aiAssistantUpdateConversation',\n summary: 'Update an existing conversation.',\n description:\n 'Accepts a partial body containing any of `title`, `status`, `pageContext`. Setting ' +\n '`status` to `closed` archives the conversation while keeping its transcript intact. ' +\n 'View-only callers can update only their own conversations; conversation managers can ' +\n 'update conversations in the same tenant/organization.',\n responses: [\n {\n status: 200,\n description: 'Updated conversation summary.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n DELETE: {\n operationId: 'aiAssistantDeleteConversation',\n summary: 'Soft-delete a conversation and its messages.',\n description:\n 'View-only callers can delete only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can delete conversations in the same tenant/organization. ' +\n 'Marks the conversation row and every undeleted message row with a `deleted_at` timestamp ' +\n 'in one transaction. The transcript remains in the database for audit/restore until a future ' +\n 'retention worker hard-deletes it.',\n responses: [\n {\n status: 200,\n description: 'Soft-delete acknowledgment.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n PATCH: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n const canManageConversations = hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n const url = new URL(req.url)\n const queryResult = aiChatConversationTranscriptQuerySchema.safeParse({\n limit: url.searchParams.get('limit') ?? undefined,\n before: url.searchParams.get('before') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const transcript = await repo.getTranscript(\n callerCtx.conversationId,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n limit: queryResult.data.limit,\n before: queryResult.data.before ?? null,\n },\n )\n if (!transcript) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n const participantCount = await repo.getParticipantCount(\n callerCtx.tenantId,\n callerCtx.organizationId,\n callerCtx.conversationId,\n )\n return NextResponse.json({\n conversation: serializeAiChatConversation(transcript.conversation, {\n callerUserId: callerCtx.userId,\n participantCount,\n }),\n messages: transcript.messages.map(serializeAiChatMessage),\n nextCursor: transcript.nextCursor,\n })\n } catch (error) {\n logger.error('AI Conversation GET \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function PATCH(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n const parseResult = aiChatConversationUpdateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation patch.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const row = await repo.update(\n callerCtx.conversationId,\n parseResult.data,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n return NextResponse.json(serializeAiChatConversation(row))\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n logger.error('AI Conversation PATCH \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to update conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n await repo.softDelete(callerCtx.conversationId, {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n logger.error('AI Conversation DELETE \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to delete conversation.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
1
2
|
import { NextResponse } from "next/server";
|
|
2
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
createConversationStorage,
|
|
8
9
|
serializeAiChatConversation
|
|
9
10
|
} from "../../../../lib/conversation-storage.js";
|
|
11
|
+
const logger = createLogger("ai_assistant");
|
|
10
12
|
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
11
13
|
const openApi = {
|
|
12
14
|
tag: "AI Assistant",
|
|
@@ -92,7 +94,7 @@ async function POST(req) {
|
|
|
92
94
|
if (error instanceof Error && error.name === "AiChatConversationAccessError") {
|
|
93
95
|
return jsonError(404, error.message, "conversation_not_found");
|
|
94
96
|
}
|
|
95
|
-
|
|
97
|
+
logger.error("AI Conversation Import \u2014 Failure", { err: error });
|
|
96
98
|
return jsonError(
|
|
97
99
|
500,
|
|
98
100
|
error instanceof Error ? error.message : "Failed to import conversation.",
|