feat: LangGraph integration with observability and startup scripts#3
feat: LangGraph integration with observability and startup scripts#3
Conversation
- Implement LangGraph supervisor agent with stateful workflow - Add Redis and Postgres checkpointers for state persistence - Create Langfuse integration for tracing and scoring - Add dual-mode scoring system (LLM + rule-based fallback) - Create one-command startup scripts and Makefile - Add ADR document for architecture decisions - Fix TypeScript compilation issues - Update README with infrastructure management docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
❌ Deploy Preview for ai-sdk failed. Why did it fail? →
|
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 20707117 | Triggered | Generic Password | 3537653 | docker-compose.yml | View secret |
| 26865273 | Triggered | JSON Web Token | a000842 | tests/unit/supabase.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
📝 WalkthroughWalkthroughAdds a LangGraph-driven supervisor, MCP-style tools (in-memory, Supabase), multi-backend checkpointer (Redis/Postgres/memory), Langfuse observability and scoring, LLM provider abstraction (OpenAI/Ollama), Supabase client/server integration, infra automation (Makefile, docker-compose, scripts), comprehensive DB schema/seed, tests, and extensive docs/ADRs. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Supervisor as Supervisor\n(LangGraph)
participant Checkpointer as Checkpointer\n(Redis/Postgres/Memory)
participant ToolExec as Tool Executor
participant Services as External Services\n(Qdrant/Embedding/LLM)
participant Langfuse as Langfuse
Client->>Supervisor: send message + threadId + userId
activate Supervisor
Supervisor->>Langfuse: createAgentTrace(input)
Supervisor->>Supervisor: classify_intent
alt intent requires tools
Supervisor->>Supervisor: generate_tool_calls
loop for each tool call
Supervisor->>ToolExec: executeTool(name,args)
ToolExec->>Services: embedding/semantic search/LLM
Services-->>ToolExec: tool result
ToolExec-->>Supervisor: tool result
Supervisor->>Checkpointer: persist intermediate state
Supervisor->>Langfuse: recordToolExecution
end
else direct response
Supervisor->>Supervisor: direct_response
end
Supervisor->>Supervisor: generate_response
Supervisor->>Langfuse: recordGeneration / addTraceScore
Supervisor->>Checkpointer: save final state
Supervisor-->>Client: return response + state
deactivate Supervisor
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @Aparnap2, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly upgrades the Smart Commerce Agent by integrating a robust LangGraph-based supervisor agent for sophisticated workflow orchestration and state management. It introduces persistent conversation state using Redis and PostgreSQL checkpointers, enhances observability with Langfuse for tracing and scoring, and streamlines development and deployment with new one-command startup scripts. These changes aim to make the agent more reliable, observable, and easier to manage, while also documenting key architectural choices. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a significant set of features, including LangGraph integration for agent orchestration, state persistence with Redis and Postgres, Langfuse for observability, and a comprehensive set of startup scripts and documentation. The changes are well-structured and the documentation (ADRs, README) is excellent. My review focuses on improving security, robustness, and maintainability by addressing hardcoded secrets and URLs, fixing a critical bug in Redis communication, and pointing out several areas with mock/placeholder implementations that need to be completed.
| async function redisGet(key: string): Promise<string | null> { | ||
| try { | ||
| const response = await fetch('http://localhost:6379', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(['GET', key]), | ||
| }); | ||
| const data = await response.json(); | ||
| return data[0] || null; | ||
| } catch { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The redisGet function incorrectly uses fetch with an HTTP POST request to communicate with Redis. Redis uses the RESP protocol, not HTTP, so this implementation will fail at runtime. You should use a proper Redis client library like ioredis (which is already a dependency) to interact with the Redis server. A single client instance should be created at the module level and reused across function calls.
| * Node: Generate tool calls based on intent | ||
| */ | ||
| async function generateToolCalls(state: typeof StateAnnotation.State): Promise<Partial<typeof StateAnnotation.State>> { | ||
| const lastMessage = state.messages[state.messages.length - 1]?.content || ''; | ||
| const intent = state.intent?.intent; | ||
|
|
||
| console.log(`[ToolAgent] 🔧 Generating tool calls for: ${intent}`); | ||
|
|
||
| const toolCalls: any[] = []; | ||
|
|
||
| switch (intent) { | ||
| case 'product_search': { | ||
| toolCalls.push({ | ||
| id: `call-${Date.now()}-1`, | ||
| type: 'function', | ||
| function: { | ||
| name: 'product_search', | ||
| arguments: JSON.stringify({ | ||
| query: lastMessage, | ||
| limit: 10, | ||
| minScore: 0.5, | ||
| }), | ||
| }, | ||
| }); | ||
| break; | ||
| } | ||
|
|
||
| case 'inventory_check': { | ||
| // Try to extract product IDs from message | ||
| const productIdMatch = lastMessage.match(/[A-Z]{2,3}-?\d{3,}/g) || ['PROD-001', 'PROD-002']; | ||
| toolCalls.push({ | ||
| id: `call-${Date.now()}-1`, | ||
| type: 'function', | ||
| function: { | ||
| name: 'inventory_check', | ||
| arguments: JSON.stringify({ | ||
| productIds: productIdMatch.slice(0, 5), | ||
| location: 'main-warehouse', | ||
| }), | ||
| }, | ||
| }); | ||
| break; | ||
| } | ||
|
|
||
| case 'order_inquiry': { | ||
| const emailMatch = lastMessage.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/); | ||
| toolCalls.push({ | ||
| id: `call-${Date.now()}-1`, | ||
| type: 'function', | ||
| function: { | ||
| name: 'order_lookup', | ||
| arguments: JSON.stringify({ | ||
| email: emailMatch?.[0] || undefined, | ||
| limit: 5, | ||
| }), | ||
| }, | ||
| }); | ||
| break; | ||
| } | ||
|
|
||
| case 'refund_request': { | ||
| toolCalls.push({ | ||
| id: `call-${Date.now()}-1`, | ||
| type: 'function', | ||
| function: { | ||
| name: 'refund_request', | ||
| arguments: JSON.stringify({ | ||
| orderId: 'ORD-001', // Would extract from message | ||
| amount: 0, // Would calculate from order | ||
| reason: 'Customer requested refund', | ||
| idempotencyKey: crypto.randomUUID(), | ||
| }), | ||
| }, | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // Note: LangGraph imports commented out due to API version mismatch | ||
| // import { | ||
| // StateGraph, | ||
| // END, | ||
| // START, | ||
| // Annotation, | ||
| // CompiledStateGraph, | ||
| // } from '@langchain/langgraph'; | ||
| // import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; | ||
| // import { SystemMessage, HumanMessage, AIMessage, ToolMessage } from '@langchain/core/messages'; | ||
| if (toolCalls.length > 0) { | ||
| console.log(`[ToolAgent] ✅ Generated ${toolCalls.length} tool calls`); | ||
| } | ||
|
|
||
| return { pendingToolCalls: toolCalls }; | ||
| } |
There was a problem hiding this comment.
The generateToolCalls function contains placeholder/mock logic for several intents:
inventory_check: Falls back to hardcoded product IDs, which can cause incorrect behavior. It would be better to not make a tool call if no ID is found.refund_request: Uses hardcodedorderIdandamount.
This logic should be fully implemented to extract necessary arguments from the conversation context to make these tools functional.
| const embedResponse = await fetch('http://localhost:11434/api/embeddings', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| model: 'nomic-embed-text', | ||
| prompt: query, | ||
| }), | ||
| }); | ||
|
|
||
| if (!embedResponse.ok) { | ||
| throw new Error('Failed to generate embedding'); | ||
| } | ||
|
|
||
| const embedData = await embedResponse.json(); | ||
| const embedding = embedData.embedding; | ||
|
|
||
| // 2. Search Qdrant for similar products | ||
| const qdrantResponse = await fetch('http://localhost:6333/collections/products/points/search', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| ...(process.env.QDRANT_API_KEY && { 'Authorization': `Bearer ${process.env.QDRANT_API_KEY}` }), | ||
| }, | ||
| body: JSON.stringify({ | ||
| query: embedding, | ||
| limit, | ||
| score_threshold: minScore, | ||
| with_payload: true, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
The URLs for the Ollama and Qdrant services are hardcoded. To improve configurability and align with best practices, these should be sourced from environment variables, similar to how env.OLLAMA_BASE_URL is used elsewhere in the project. You should use env.OLLAMA_BASE_URL for Ollama and add a new environment variable like env.QDRANT_URL for Qdrant.
| -e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/smart_commerce \ | ||
| -e REDIS_URL=redis://host.docker.internal:6379 \ |
There was a problem hiding this comment.
The Langfuse container is configured to connect to Postgres and Redis via host.docker.internal. This relies on port mapping to the host and is less efficient and less portable than direct container-to-container communication. The recommended approach is to create a user-defined Docker network, attach all services to it, and then use the service container names for discovery (e.g., postgresql://postgres:postgres@smart-commerce-postgres:5432/smart_commerce).
| -e NEXTAUTH_SECRET=your-secret-key-change-in-production \ | ||
| -e SALT=your-salt-change-in-production \ |
There was a problem hiding this comment.
Hardcoding secrets like NEXTAUTH_SECRET and SALT is a security risk. These values should be sourced from environment variables, and the script should ideally fail or warn if they are not set. This prevents committing sensitive information to version control. Additionally, the multi-line docker run command is missing line continuations (\) for these environment variables, which will cause the script to fail.
| -e NEXTAUTH_SECRET=your-secret-key-change-in-production \ | |
| -e SALT=your-salt-change-in-production \ | |
| -e NEXTAUTH_SECRET=${NEXTAUTH_SECRET?Error: NEXTAUTH_SECRET is not set} \ | |
| -e SALT=${SALT?Error: SALT is not set} \ |
| @docker exec sca-ollama ollama pull qwen2.5-coder:3b | ||
| @docker exec sca-ollama ollama pull nomic-embed-text |
| try { | ||
| // Use RAG document search instead of simulated web search | ||
| const { documentSearch } = await import('../../../lib/rag/service.ts'); | ||
| const { documentSearch } = await import('../../../lib/rag/service'); |
| try { | ||
| // Use RAG vector search instead of simulated results | ||
| const { vectorSearch } = await import('../../../lib/rag/service.ts'); | ||
| const { vectorSearch } = await import('../../../lib/rag/service'); |
| export async function orderLookup(input: OrderLookupInput): Promise<{ | ||
| success: boolean; | ||
| orders: OrderLookupResult[]; | ||
| error?: string; | ||
| }> { | ||
| console.log(`[Tool] 📋 OrderLookup:`, input); | ||
|
|
||
| // Simulated order lookup (would use Prisma in production) | ||
| const mockOrders: OrderLookupResult[] = [ | ||
| { | ||
| id: 'ORD-001', | ||
| customerEmail: input.email || 'customer@example.com', | ||
| products: [ | ||
| { name: 'Laptop Pro 15', quantity: 1, price: 1299.99 }, | ||
| { name: 'Wireless Mouse', quantity: 2, price: 49.99 }, | ||
| ], | ||
| total: 1399.97, | ||
| status: 'shipped', | ||
| orderDate: '2026-01-25', | ||
| trackingNumber: 'TRK-123456', | ||
| }, | ||
| { | ||
| id: 'ORD-002', | ||
| customerEmail: input.email || 'customer@example.com', | ||
| products: [ | ||
| { name: 'USB-C Hub', quantity: 1, price: 79.99 }, | ||
| ], | ||
| total: 79.99, | ||
| status: 'processing', | ||
| orderDate: '2026-01-27', | ||
| }, | ||
| ]; | ||
|
|
||
| return { | ||
| success: true, | ||
| orders: mockOrders, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Refund request tool | ||
| */ | ||
| export async function refundRequest(input: RefundRequestInput): Promise<{ | ||
| success: boolean; | ||
| result: RefundResult; | ||
| }> { | ||
| console.log(`[Tool] 💰 RefundRequest: Order ${input.orderId}, Amount $${input.amount}`); | ||
|
|
||
| // Simulated refund processing | ||
| const result: RefundResult = { | ||
| success: true, | ||
| refundId: `REF-${Date.now()}`, | ||
| status: 'pending', | ||
| amount: input.amount, | ||
| message: 'Refund request submitted for review', | ||
| }; | ||
|
|
||
| return { | ||
| success: true, | ||
| result, | ||
| }; | ||
| } |
|
|
||
| import { queryDatabase } from '../tools/database.js'; | ||
| import { logger } from '../redis/logger.ts'; | ||
| import { logger } from '../redis/logger'; |
There was a problem hiding this comment.
Actionable comments posted: 12
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In `@lib/agents/supervisor.ts`:
- Around line 301-316: The refund tool call is using hardcoded invalid values
(orderId: 'ORD-001' and amount: 0) which will fail RefundRequestInputSchema
(requires a positive number). Update the case 'refund_request' block that builds
toolCalls so it extracts the actual order ID and refund amount from the incoming
message/context (e.g., parse from the user message or look up the order in the
agent state), ensure amount is a positive number (compute or validate before
stringifying), and only push the tool call when the values pass validation; keep
using crypto.randomUUID() for idempotencyKey and the same function name
'refund_request'. Ensure any validation errors are handled (skip or surface to
the user) rather than sending an invalid payload.
- Around line 182-206: The Ollama fetch call that creates "response" currently
has no timeout; wrap the fetch in an AbortController: create an AbortController
before the fetch, pass controller.signal into fetch options, and set a
setTimeout to call controller.abort() after a safe timeout (e.g., 10s), then
clear the timer after the fetch resolves; ensure you handle
AbortError/rejections properly where "response" is awaited. Apply the same
pattern to the other Ollama fetch calls referenced in processToolResults and
directResponseNode so all LLM requests use an AbortController-based timeout.
In `@lib/agents/tools.ts`:
- Around line 287-299: The redisGet helper is wrong because it tries to talk to
Redis over HTTP (fetch to localhost:6379) and thus always fails; fix by
replacing redisGet with a proper Redis client call (e.g., use ioredis or
node-redis) inside the redisGet function: instantiate/connect a client (or reuse
an existing client), call the GET command for the given key, return the string
or null, and ensure errors are caught and logged; alternatively if you don't
want a new dependency, remove redisGet and document that Redis integration is
pending and rely on the mock inventory instead.
In `@lib/observability/langfuse.ts`:
- Around line 110-119: The no-op Langfuse client returned by createNoOpClient is
missing trace methods used by callers; update createNoOpClient so the trace()
result also exposes span(), event(), generation(), and score() (in addition to
end, flush, update) and have each of those return appropriate no-op objects
(e.g., objects with end, flush, update and any methods callers expect) so
createNodeSpan, recordToolExecution, recordGeneration, and addTraceScore can
call them without runtime errors; keep the overall return cast to Langfuse and
match the existing trace() shape used elsewhere.
- Around line 131-133: The current isLangfuseEnabled() (which checks
_state.isInitialized && _state.client !== null) returns true for no-op clients;
add a new boolean flag _state.isEnabled to represent "real" enablement, set
_state.isEnabled = true only when a real Langfuse client is successfully created
(in the successful initialization path where the real client is assigned), and
set it to false when falling back to the no-op client (including the API key
missing and initialization failure branches that currently set
_state.isInitialized = true). Update isLangfuseEnabled() to return
_state.isEnabled (or _state.isInitialized && _state.isEnabled) and ensure all
places that created or replaced _state.client (the no-op creation and
error-handling blocks) also set _state.isEnabled appropriately.
In `@lib/redis/langgraph-checkpoint.ts`:
- Around line 136-141: The RedisSaver is created without a TTL option so
checkpoints never expire; update the RedisSaver initialization (symbol:
RedisSaver) to pass defaultTTL in minutes (compute ttlMinutes =
Math.ceil(ttlSeconds/60) from ttlSeconds which comes from config?.sessionTTL or
86400) and include refreshOnRead: true in the options, and remove or change the
misleading comment "Set TTL using client commands" since you are setting TTL via
the RedisSaver options rather than issuing Redis commands.
- Around line 315-337: The healthCheckPostgres function must ensure Postgres
clients are always released and _postgresState.isConnected is updated on both
success and failure: after obtaining a client via _postgresState.pool.connect()
use a try/finally around client.query('SELECT 1') so client.release() is called
in the finally block, set _postgresState.lastHealthCheck and
_postgresState.isConnected = true on successful query, and in the catch block
set _postgresState.isConnected = false before returning the failure object;
leave pool.connect() errors untouched (no client to release).
- Around line 181-230: The initializePostgresCheckpointer function leaks a
client if client.query throws and leaks the pool on any init failure; declare
the Pool variable (pool) before the try so it is accessible in catch, wrap the
client connect/query in a try-finally to always call client.release(), and in
the catch block call await pool.end() (and reset _postgresState as appropriate)
before rethrowing the error so the Postgres pool is closed on initialization
failure; reference initializePostgresCheckpointer, Pool, client.release(),
pool.end(), PostgresSaver.setup and _postgresState when making these changes.
- Around line 105-155: The initialization Promise in initializeRedisCheckpointer
currently waits indefinitely for client 'ready'; wrap that wait in a timeout via
Promise.race (e.g., a timeout Promise that rejects after a configurable
milliseconds, defaulting to something reasonable) so the function fails fast if
Redis doesn't become ready, and on any catch path ensure the created client is
properly closed (call client.quit() or client.disconnect() and remove listeners)
before rethrowing; also update _redisState only after successful init and ensure
the timeout cleanup clears any pending timers/listeners to avoid leaks.
In `@Makefile`:
- Around line 106-109: Add a Makefile-level shell setting to ensure the db-reset
target's bash-specific constructs run reliably: add "SHELL := /bin/bash" at the
top of the Makefile so the db-reset target (the target named db-reset that uses
read -p, [[ ]], and $$REPLY) executes under bash; alternatively, if you prefer
POSIX portability, replace the bashisms in the db-reset recipe with
POSIX-compliant equivalents (use plain read without -p, single [ ] for tests,
and POSIX pattern matching) and keep the SHELL change only if you want to
enforce bash globally.
- Around line 112-116: The Makefile target ollama-pull is using the wrong
container name (sca-ollama); update the two docker exec commands within the
ollama-pull target to use the actual container name smart-commerce-ollama so
they match test-langgraph.sh and the running container (change occurrences of
sca-ollama to smart-commerce-ollama in the ollama-pull recipe).
In `@scripts/start-infrastructure.sh`:
- Around line 153-154: The port mapping maps both container ports 6333 and 6334
to the same host port variable QDRANT_PORT; update the second mapping so host
and container ports are distinct (e.g., keep "-p ${QDRANT_PORT}:6333" for HTTP
and change the gRPC mapping to "-p ${QDRANT_GRPC_PORT}:6334" or similar), ensure
you introduce/use a new host variable (QDRANT_GRPC_PORT) in the script or
environment and update any references or defaults so 6334 is exposed properly
for gRPC.
🟡 Minor comments (12)
scripts/test-langgraph.sh-27-31 (1)
27-31: Remove or wire unused config vars.
Line 29 and Line 30 are unused and can mislead maintenance. Consider removing or actually using them to enforce a timeout/thread ID.🧹 Proposed cleanup
-COMPOSE_FILE="docker-compose.yml" -TEST_TIMEOUT=300 # 5 minutes -THREAD_ID="test-thread-$(date +%s)" +COMPOSE_FILE="docker-compose.yml"lib/env.js-38-46 (1)
38-46: Guard LANGFUSE_SAMPLING_RATE against NaN/out-of-range values.
Line 46 can yield NaN or values outside 0–1, which may cause downstream issues. Clamp and default if invalid.✅ Suggested hardening
-const optionalEnvVars = { +const samplingRateRaw = parseFloat(process.env.LANGFUSE_SAMPLING_RATE || '1.0'); +const samplingRate = Number.isFinite(samplingRateRaw) + ? Math.min(Math.max(samplingRateRaw, 0), 1) + : 1.0; + +const optionalEnvVars = { NODE_ENV: process.env.NODE_ENV || 'development', GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY, @@ - LANGFUSE_SAMPLING_RATE: parseFloat(process.env.LANGFUSE_SAMPLING_RATE || '1.0'), + LANGFUSE_SAMPLING_RATE: samplingRate, };docs/STRATEGIC_INTEGRATION_PLAN.md-69-77 (1)
69-77: Update documentation versions to match package.json specifications.Lines 75–77 specify
@langchain/langgraph@^0.2.74and@langchain/langgraph-checkpoint-redis@^1.0.0, but package.json declares@langchain/langgraph@^0.2.21and@langchain/langgraph-checkpoint-redis@^1.0.1. Update the doc to reference the actual package.json versions or clearly label these as example versions for illustration purposes.README.md-148-148 (1)
148-148: Potential port conflict: Langfuse and Next.js both use port 3000.Langfuse is configured on port 3000, which conflicts with the default Next.js development server port. This will cause issues when running
pnpm devafter starting infrastructure.Consider changing the Langfuse port mapping to avoid conflict (e.g., 3001 or 4000).
scripts/start-infrastructure.sh-203-203 (1)
203-203: PostgreSQL version mismatch with docker-compose.yml.This script uses
pgvector/pgvector:pg16whiledocker-compose.ymlusespgvector/pgvector:pg15. This inconsistency could cause issues when switching between manual and docker-compose startup methods.Proposed fix to align with docker-compose.yml
- pgvector/pgvector:pg16 + pgvector/pgvector:pg15lib/agents/supervisor.ts-208-213 (1)
208-213: Add error handling for JSON.parse.If the LLM returns malformed JSON,
JSON.parse(content)will throw, causing the entire classification to fail even though there's a catch block that assumes a different error path.Proposed fix
const data = await response.json(); const content = data.choices?.[0]?.message?.content || '{}'; - const parsed = JSON.parse(content); - const intent = parsed.intent || 'general_support'; - const confidence = parsed.confidence || 0.5; + let intent = 'general_support'; + let confidence = 0.5; + try { + const parsed = JSON.parse(content); + intent = parsed.intent || 'general_support'; + confidence = parsed.confidence || 0.5; + } catch (parseError) { + console.warn('[Supervisor] ⚠️ Failed to parse intent JSON, using defaults'); + }lib/agents/tools.ts-142-149 (1)
142-149: Hardcoded Ollama URL reduces configurability.The embedding endpoint uses a hardcoded
http://localhost:11434instead of the environment variable used elsewhere (env.OLLAMA_BASE_URL).Proposed fix
+import { env } from '@/lib/env'; + // In productSearch function: - const embedResponse = await fetch('http://localhost:11434/api/embeddings', { + const embedResponse = await fetch(`${env.OLLAMA_BASE_URL}/api/embeddings`, {lib/agents/tools.ts-159-171 (1)
159-171: Hardcoded Qdrant URL reduces configurability.Similar to the Ollama URL, the Qdrant endpoint should use an environment variable.
Proposed fix
+const QDRANT_URL = process.env.QDRANT_URL || 'http://localhost:6333'; + // In productSearch function: - const qdrantResponse = await fetch('http://localhost:6333/collections/products/points/search', { + const qdrantResponse = await fetch(`${QDRANT_URL}/collections/products/points/search`, {lib/observability/scoring.ts-64-155 (1)
64-155: Fix negative latency on LLM fallback.
Line 153 computesstartTime - Date.now()which produces a negative latency.🛠️ Proposed fix
- return createFallbackScoring(query, response, startTime - Date.now()); + return createFallbackScoring(query, response, Date.now() - startTime);lib/redis/langgraph-checkpoint.ts-361-376 (1)
361-376: Expose the real session TTL instead of a hardcoded default.
sessionTTLis always86400even when config overrides it. Consider tracking the configured value in module state and returning that.🛠️ Proposed fix
+let _sessionTTL = 86400; @@ - const ttlSeconds = config?.sessionTTL || 86400; + const ttlSeconds = config?.sessionTTL || 86400; + _sessionTTL = ttlSeconds; @@ - // Initialize checkpoint tables (creates if not exists) + _sessionTTL = config?.sessionTTL || 86400; + // Initialize checkpoint tables (creates if not exists) @@ - sessionTTL: 86400, // Default TTL + sessionTTL: _sessionTTL,lib/observability/langfuse.ts-50-63 (1)
50-63: AllowsampleRate = 0to disable sampling.Line 62 uses
|| 1.0, which converts0into1.0due to JavaScript's falsy coercion. SincesampleRatehas a valid range of 0-1 (per type definition), explicit zero should be preserved. Use nullish coalescing throughout:- const sampleRate = (config?.sampleRate ?? env.LANGFUSE_SAMPLING_RATE) || 1.0; + const sampleRate = config?.sampleRate ?? env.LANGFUSE_SAMPLING_RATE ?? 1.0;lib/redis/langgraph-checkpoint.ts-288-309 (1)
288-309: Update Redis connection state on failed health checks.The
healthCheckRedis()function returnsconnected: falseon ping failure but doesn't update_redisState.isConnected. This causeshealthCheckAll()to report incorrectactiveTypebased on stale state—if a previous connection succeeded,isConnectedremains true even after a failed ping.🛠️ Proposed fix
} catch (error) { + _redisState.isConnected = false; return { healthy: false, latency: Date.now() - start, connected: false }; }
🧹 Nitpick comments (11)
lib/components/tool-call-display.tsx (1)
168-174: UnusedanimationSpeedprop.The
animationSpeedprop is defined inToolCallDisplayProps(line 44) and destructured here with a default value, but it's never passed toExecutingSpinneror used anywhere in this component. This appears to be dead code.Consider either:
- Passing it to
ToolCallEventItemand then toExecutingSpinner, or- Removing it from the props interface if not needed.
♻️ Option 1: Wire up the animationSpeed prop
interface ToolCallEventItemProps { event: ToolCallEvent; showExecutionTime: boolean; onClick?: () => void; + animationSpeed?: number; } const ToolCallEventItem: React.FC<ToolCallEventItemProps> = ({ event, showExecutionTime, onClick, + animationSpeed = 200, }) => { // ... return ( // ... <span className={`${colorClass} text-lg w-6 text-center`}> - {event.status === 'executing' ? <ExecutingSpinner /> : icon} + {event.status === 'executing' ? <ExecutingSpinner speed={animationSpeed} /> : icon} </span> // ... ); }Then update the call site in
ToolCallDisplay:<ToolCallEventItem key={event.id} event={event} showExecutionTime={showExecutionTime} + animationSpeed={animationSpeed} onClick={onEventClick ? () => onEventClick(event) : undefined} />package.json (2)
40-40: Consider movingplaywrightto devDependencies.
playwright(line 40) is a testing library and is typically placed indevDependenciesrather thandependencies. This keeps production bundles smaller and clarifies the package's purpose.
67-73: Note: Dual test framework setup (Jest + Vitest).The project now includes both Jest (
jest,@types/jest,@jest/globals,ts-jest,babel-jest) and Vitest (vitest,@vitejs/plugin-react,vite-tsconfig-paths). While this can work during a migration period, maintaining two test frameworks increases complexity and install times.Consider consolidating to a single framework once the migration to Vitest is complete.
README.md (2)
166-181: Add language identifier to the architecture diagram code block.The fenced code block lacks a language identifier. For ASCII diagrams, use
textorplaintext.Proposed fix
-``` +```text User Message
225-225: Duplicate heading: "Project Structure" appears twice.This heading already exists at line 38. Consider renaming this section to something more specific like "Chat Flow Architecture" or removing it to avoid confusion in document navigation.
docker-compose.yml (1)
11-13: Hardcoded database credentials are acceptable for local dev but ensure they're not used in production.The credentials
postgres:postgresare fine for local development. Ensure production deployments use environment variable substitution or secrets management.scripts/start-infrastructure.sh (3)
237-238: Hardcoded secrets with placeholder values.
NEXTAUTH_SECRETandSALTuse placeholder values. While comments indicate these should be changed, consider:
- Generating random values at runtime for local dev
- Reading from environment variables with fallback
Proposed improvement
- -e NEXTAUTH_SECRET=your-secret-key-change-in-production \ - -e SALT=your-salt-change-in-production \ + -e NEXTAUTH_SECRET="${NEXTAUTH_SECRET:-$(openssl rand -base64 32)}" \ + -e SALT="${SALT:-$(openssl rand -base64 16)}" \
60-66: Shellcheck: Declare and assign separately to avoid masking return values.When
localand command substitution are combined, the exit status of the command is masked bylocal(which always succeeds).Proposed fix for lines 60, 66, 73, 89
is_running() { - local name=$(get_container_name "$1") + local name + name=$(get_container_name "$1") docker ps --format '{{.Names}}' | grep -q "^${name}$" } exists() { - local name=$(get_container_name "$1") + local name + name=$(get_container_name "$1") docker ps -a --format '{{.Names}}' | grep -q "^${name}$" }
108-108: Quote$(pwd)to prevent word splitting.If the current directory path contains spaces, this will break.
Proposed fix
- -v $(pwd)/docker/netdata:/etc/netdata \ + -v "$(pwd)/docker/netdata":/etc/netdata \lib/agents/supervisor.ts (1)
458-461: Redundant conditional edge function.
shouldContinueAfterToolsalways returns'generate_response'regardless of state. The return type annotation'generate_response' | 'generate_response'also indicates redundancy.Either simplify to a direct edge or implement actual conditional logic.
Proposed simplification
-/** - * Conditional edge: Continue or end after tool execution - */ -function shouldContinueAfterTools(state: typeof StateAnnotation.State): 'generate_response' | 'generate_response' { - // After tools execute, always generate response - return 'generate_response'; -} // In graph construction, replace: - (workflow as any).addConditionalEdges( - 'tools', - shouldContinueAfterTools, - { - generate_response: 'generate_response', - } - ); + (workflow as any).addEdge('tools', 'generate_response');lib/agents/tools.ts (1)
20-26: Schema duplication between Zod and TOOL_DEFINITIONS.The tool parameter schemas are defined twice: once in Zod (lines 20-26, etc.) and again in JSON Schema format (lines 374-410). This creates a maintenance burden and risk of drift.
Consider generating TOOL_DEFINITIONS from Zod schemas using
zodToJsonSchemafrom thezod-to-json-schemapackage, or using LangChain's built-in Zod integration which handles this automatically.Also applies to: 374-410
lib/agents/supervisor.ts
Outdated
| const response = await fetch(`${env.OLLAMA_BASE_URL}/v1/chat/completions`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| model: env.OLLAMA_MODEL || 'qwen2.5-coder:3b', | ||
| messages: [ | ||
| { | ||
| role: 'system', | ||
| content: `You are an intent classifier for an e-commerce support system. | ||
|
|
||
| Classify the user query into one of: | ||
| - product_search: "find/show/recommend products", "what do you have" | ||
| - order_inquiry: "track/order status", "where is my order" | ||
| - inventory_check: "is X in stock", "check availability" | ||
| - refund_request: "refund/money back", "return item" | ||
| - general_support: "other questions" | ||
|
|
||
| Respond with JSON: {"intent": "...", "confidence": 0.x, "reasoning": "..."}` | ||
| }, | ||
| { role: 'user', content: lastMessage }, | ||
| ], | ||
| temperature: 0.1, | ||
| format: { type: 'json_object' }, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Add timeout to fetch calls to prevent hanging requests.
The LLM calls to Ollama have no timeout. If Ollama is slow or unresponsive, these requests will hang indefinitely, potentially causing cascading failures.
Proposed fix using AbortController
try {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 30000); // 30s timeout
+
const response = await fetch(`${env.OLLAMA_BASE_URL}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
+ signal: controller.signal,
body: JSON.stringify({
// ...
}),
});
+
+ clearTimeout(timeout);Apply similar pattern to other fetch calls in processToolResults (line 341) and directResponseNode (line 396).
🤖 Prompt for AI Agents
In `@lib/agents/supervisor.ts` around lines 182 - 206, The Ollama fetch call that
creates "response" currently has no timeout; wrap the fetch in an
AbortController: create an AbortController before the fetch, pass
controller.signal into fetch options, and set a setTimeout to call
controller.abort() after a safe timeout (e.g., 10s), then clear the timer after
the fetch resolves; ensure you handle AbortError/rejections properly where
"response" is awaited. Apply the same pattern to the other Ollama fetch calls
referenced in processToolResults and directResponseNode so all LLM requests use
an AbortController-based timeout.
| case 'refund_request': { | ||
| toolCalls.push({ | ||
| id: `call-${Date.now()}-1`, | ||
| type: 'function', | ||
| function: { | ||
| name: 'refund_request', | ||
| arguments: JSON.stringify({ | ||
| orderId: 'ORD-001', // Would extract from message | ||
| amount: 0, // Would calculate from order | ||
| reason: 'Customer requested refund', | ||
| idempotencyKey: crypto.randomUUID(), | ||
| }), | ||
| }, | ||
| }); | ||
| break; | ||
| } |
There was a problem hiding this comment.
Hardcoded refund values will fail validation.
The refund_request tool call uses amount: 0, but RefundRequestInputSchema requires z.number().positive(). This will always fail Zod validation.
Additionally, the hardcoded orderId: 'ORD-001' ignores any order ID the user might reference.
Proposed fix
case 'refund_request': {
+ // Extract order ID from message (basic pattern matching)
+ const orderIdMatch = lastMessage.match(/ORD-\d+/i);
toolCalls.push({
id: `call-${Date.now()}-1`,
type: 'function',
function: {
name: 'refund_request',
arguments: JSON.stringify({
- orderId: 'ORD-001', // Would extract from message
- amount: 0, // Would calculate from order
+ orderId: orderIdMatch?.[0] || 'UNKNOWN',
+ amount: 1, // Placeholder - should be calculated from order lookup
reason: 'Customer requested refund',
idempotencyKey: crypto.randomUUID(),
}),🤖 Prompt for AI Agents
In `@lib/agents/supervisor.ts` around lines 301 - 316, The refund tool call is
using hardcoded invalid values (orderId: 'ORD-001' and amount: 0) which will
fail RefundRequestInputSchema (requires a positive number). Update the case
'refund_request' block that builds toolCalls so it extracts the actual order ID
and refund amount from the incoming message/context (e.g., parse from the user
message or look up the order in the agent state), ensure amount is a positive
number (compute or validate before stringifying), and only push the tool call
when the values pass validation; keep using crypto.randomUUID() for
idempotencyKey and the same function name 'refund_request'. Ensure any
validation errors are handled (skip or surface to the user) rather than sending
an invalid payload.
| async function redisGet(key: string): Promise<string | null> { | ||
| try { | ||
| const response = await fetch('http://localhost:6379', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(['GET', key]), | ||
| }); | ||
| const data = await response.json(); | ||
| return data[0] || null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Critical: Redis doesn't speak HTTP - this helper will always fail.
The redisGet function attempts to communicate with Redis via HTTP POST to port 6379, but Redis uses the RESP protocol, not HTTP. This will always return null from the catch block, making the inventory check always fall back to mock data.
Proposed fix using ioredis
+import Redis from 'ioredis';
+
+// Create Redis client (singleton)
+let redisClient: Redis | null = null;
+
+function getRedisClient(): Redis {
+ if (!redisClient) {
+ redisClient = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
+ }
+ return redisClient;
+}
/**
* Redis get helper
*/
async function redisGet(key: string): Promise<string | null> {
try {
- const response = await fetch('http://localhost:6379', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(['GET', key]),
- });
- const data = await response.json();
- return data[0] || null;
+ const client = getRedisClient();
+ return await client.get(key);
} catch {
return null;
}
}Alternatively, if you want to avoid adding a dependency, remove redisGet entirely and rely on mock data for now, clearly documenting that Redis integration is pending.
🤖 Prompt for AI Agents
In `@lib/agents/tools.ts` around lines 287 - 299, The redisGet helper is wrong
because it tries to talk to Redis over HTTP (fetch to localhost:6379) and thus
always fails; fix by replacing redisGet with a proper Redis client call (e.g.,
use ioredis or node-redis) inside the redisGet function: instantiate/connect a
client (or reuse an existing client), call the GET command for the given key,
return the string or null, and ensure errors are caught and logged;
alternatively if you don't want a new dependency, remove redisGet and document
that Redis integration is pending and rely on the mock inventory instead.
| export function isLangfuseEnabled(): boolean { | ||
| return _state.isInitialized && _state.client !== null; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n lib/observability/langfuse.tsRepository: Aparnap2/smart_commerce_agent
Length of output: 7922
isLangfuseEnabled() returns true even when using the no-op client.
When API keys are missing (lines 64-71) or initialization fails (lines 96-104), a no-op client is created with isInitialized set to true. Since isLangfuseEnabled() only checks _state.isInitialized && _state.client !== null, it returns true for both real and no-op clients. Add a separate isEnabled flag to distinguish between actual enablement and initialization with degraded mode.
🛠️ Proposed fix
interface LangfuseState {
client: Langfuse | null;
isInitialized: boolean;
+ isEnabled: boolean;
lastInitTime: number;
}
@@
let _state: LangfuseState = {
client: null,
isInitialized: false,
+ isEnabled: false,
lastInitTime: 0,
};
@@
if (!publicKey || !secretKey) {
console.warn('[Langfuse] Missing API keys, using no-op client');
_state = {
client: createNoOpClient(),
isInitialized: true,
+ isEnabled: false,
lastInitTime: Date.now(),
};
return _state.client;
}
@@
_state = {
client,
isInitialized: true,
+ isEnabled: true,
lastInitTime: Date.now(),
};
@@
_state = {
client: createNoOpClient(),
isInitialized: true,
+ isEnabled: false,
lastInitTime: Date.now(),
};
return _state.client;
}
}
@@
export function isLangfuseEnabled(): boolean {
- return _state.isInitialized && _state.client !== null;
+ return _state.isEnabled;
}🤖 Prompt for AI Agents
In `@lib/observability/langfuse.ts` around lines 131 - 133, The current
isLangfuseEnabled() (which checks _state.isInitialized && _state.client !==
null) returns true for no-op clients; add a new boolean flag _state.isEnabled to
represent "real" enablement, set _state.isEnabled = true only when a real
Langfuse client is successfully created (in the successful initialization path
where the real client is assigned), and set it to false when falling back to the
no-op client (including the API key missing and initialization failure branches
that currently set _state.isInitialized = true). Update isLangfuseEnabled() to
return _state.isEnabled (or _state.isInitialized && _state.isEnabled) and ensure
all places that created or replaced _state.client (the no-op creation and
error-handling blocks) also set _state.isEnabled appropriately.
| export async function initializePostgresCheckpointer(config?: CheckpointConfig): Promise<PostgresSaver> { | ||
| if (_postgresState.checkpointer) { | ||
| console.log('[CheckpointService] Postgres checkpointer already initialized'); | ||
| return _postgresState.checkpointer; | ||
| } | ||
|
|
||
| async list( | ||
| threadId: string, | ||
| options?: { limit?: number; before?: string; metadata?: Record<string, unknown> } | ||
| ): Promise<Array<SerializedCheckpoint>> { | ||
| return []; | ||
| console.log('[CheckpointService] Initializing Postgres checkpointer...'); | ||
|
|
||
| try { | ||
| const poolOptions = buildPostgresPoolOptions(config); | ||
| const pool = new Pool(poolOptions); | ||
|
|
||
| pool.on('error', (err) => { | ||
| console.error('[CheckpointService] Postgres pool error:', err.message); | ||
| _postgresState.isConnected = false; | ||
| }); | ||
|
|
||
| pool.on('connect', () => { | ||
| console.log('[CheckpointService] Postgres client connected'); | ||
| _postgresState.isConnected = true; | ||
| }); | ||
|
|
||
| // Test connection | ||
| const client = await pool.connect(); | ||
| await client.query('SELECT 1'); | ||
| client.release(); | ||
|
|
||
| // Create the official LangGraph Postgres checkpointer | ||
| const checkpointer = new PostgresSaver(pool, undefined, { | ||
| schema: config?.postgresSchema || 'public', | ||
| }); | ||
|
|
||
| // Initialize checkpoint tables (creates if not exists) | ||
| await checkpointer.setup(); | ||
|
|
||
| _postgresState = { | ||
| pool, | ||
| checkpointer, | ||
| isConnected: true, | ||
| lastHealthCheck: Date.now(), | ||
| }; | ||
|
|
||
| console.log('[CheckpointService] ✅ Postgres checkpointer initialized'); | ||
|
|
||
| return checkpointer; | ||
| } catch (error) { | ||
| console.error('[CheckpointService] ❌ Failed to initialize Postgres checkpointer:', error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n lib/redis/langgraph-checkpoint.ts | head -250Repository: Aparnap2/smart_commerce_agent
Length of output: 8943
🏁 Script executed:
rg "client\.release()" lib/redis/langgraph-checkpoint.ts -B 3 -A 3Repository: Aparnap2/smart_commerce_agent
Length of output: 531
🏁 Script executed:
rg "pool\.end()" lib/redis/langgraph-checkpoint.ts -B 2 -A 2Repository: Aparnap2/smart_commerce_agent
Length of output: 276
Wrap the connection test in try-finally to guarantee client release, and close pool on initialization failure.
Lines 204-206 don't ensure client.release() is called if client.query() throws, leaking the connection. Additionally, if initialization fails at any point, the pool created on line 191 is never closed, causing pool resource exhaustion on repeated init attempts.
Move the pool declaration outside the try block and add cleanup in the catch block:
🛠️ Proposed fix
export async function initializePostgresCheckpointer(config?: CheckpointConfig): Promise<PostgresSaver> {
if (_postgresState.checkpointer) {
console.log('[CheckpointService] Postgres checkpointer already initialized');
return _postgresState.checkpointer;
}
console.log('[CheckpointService] Initializing Postgres checkpointer...');
+ let pool: Pool | null = null;
try {
const poolOptions = buildPostgresPoolOptions(config);
- const pool = new Pool(poolOptions);
+ pool = new Pool(poolOptions);
pool.on('error', (err) => {
console.error('[CheckpointService] Postgres pool error:', err.message);
_postgresState.isConnected = false;
});
pool.on('connect', () => {
console.log('[CheckpointService] Postgres client connected');
_postgresState.isConnected = true;
});
// Test connection
const client = await pool.connect();
+ try {
await client.query('SELECT 1');
+ } finally {
client.release();
+ }
// Create the official LangGraph Postgres checkpointer
const checkpointer = new PostgresSaver(pool, undefined, {
schema: config?.postgresSchema || 'public',
});
// Initialize checkpoint tables (creates if not exists)
await checkpointer.setup();
_postgresState = {
pool,
checkpointer,
isConnected: true,
lastHealthCheck: Date.now(),
};
console.log('[CheckpointService] ✅ Postgres checkpointer initialized');
return checkpointer;
} catch (error) {
+ if (pool) {
+ await pool.end().catch(() => undefined);
+ }
console.error('[CheckpointService] ❌ Failed to initialize Postgres checkpointer:', error);
throw error;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function initializePostgresCheckpointer(config?: CheckpointConfig): Promise<PostgresSaver> { | |
| if (_postgresState.checkpointer) { | |
| console.log('[CheckpointService] Postgres checkpointer already initialized'); | |
| return _postgresState.checkpointer; | |
| } | |
| async list( | |
| threadId: string, | |
| options?: { limit?: number; before?: string; metadata?: Record<string, unknown> } | |
| ): Promise<Array<SerializedCheckpoint>> { | |
| return []; | |
| console.log('[CheckpointService] Initializing Postgres checkpointer...'); | |
| try { | |
| const poolOptions = buildPostgresPoolOptions(config); | |
| const pool = new Pool(poolOptions); | |
| pool.on('error', (err) => { | |
| console.error('[CheckpointService] Postgres pool error:', err.message); | |
| _postgresState.isConnected = false; | |
| }); | |
| pool.on('connect', () => { | |
| console.log('[CheckpointService] Postgres client connected'); | |
| _postgresState.isConnected = true; | |
| }); | |
| // Test connection | |
| const client = await pool.connect(); | |
| await client.query('SELECT 1'); | |
| client.release(); | |
| // Create the official LangGraph Postgres checkpointer | |
| const checkpointer = new PostgresSaver(pool, undefined, { | |
| schema: config?.postgresSchema || 'public', | |
| }); | |
| // Initialize checkpoint tables (creates if not exists) | |
| await checkpointer.setup(); | |
| _postgresState = { | |
| pool, | |
| checkpointer, | |
| isConnected: true, | |
| lastHealthCheck: Date.now(), | |
| }; | |
| console.log('[CheckpointService] ✅ Postgres checkpointer initialized'); | |
| return checkpointer; | |
| } catch (error) { | |
| console.error('[CheckpointService] ❌ Failed to initialize Postgres checkpointer:', error); | |
| throw error; | |
| } | |
| } | |
| export async function initializePostgresCheckpointer(config?: CheckpointConfig): Promise<PostgresSaver> { | |
| if (_postgresState.checkpointer) { | |
| console.log('[CheckpointService] Postgres checkpointer already initialized'); | |
| return _postgresState.checkpointer; | |
| } | |
| console.log('[CheckpointService] Initializing Postgres checkpointer...'); | |
| let pool: Pool | null = null; | |
| try { | |
| const poolOptions = buildPostgresPoolOptions(config); | |
| pool = new Pool(poolOptions); | |
| pool.on('error', (err) => { | |
| console.error('[CheckpointService] Postgres pool error:', err.message); | |
| _postgresState.isConnected = false; | |
| }); | |
| pool.on('connect', () => { | |
| console.log('[CheckpointService] Postgres client connected'); | |
| _postgresState.isConnected = true; | |
| }); | |
| // Test connection | |
| const client = await pool.connect(); | |
| try { | |
| await client.query('SELECT 1'); | |
| } finally { | |
| client.release(); | |
| } | |
| // Create the official LangGraph Postgres checkpointer | |
| const checkpointer = new PostgresSaver(pool, undefined, { | |
| schema: config?.postgresSchema || 'public', | |
| }); | |
| // Initialize checkpoint tables (creates if not exists) | |
| await checkpointer.setup(); | |
| _postgresState = { | |
| pool, | |
| checkpointer, | |
| isConnected: true, | |
| lastHealthCheck: Date.now(), | |
| }; | |
| console.log('[CheckpointService] ✅ Postgres checkpointer initialized'); | |
| return checkpointer; | |
| } catch (error) { | |
| if (pool) { | |
| await pool.end().catch(() => undefined); | |
| } | |
| console.error('[CheckpointService] ❌ Failed to initialize Postgres checkpointer:', error); | |
| throw error; | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@lib/redis/langgraph-checkpoint.ts` around lines 181 - 230, The
initializePostgresCheckpointer function leaks a client if client.query throws
and leaks the pool on any init failure; declare the Pool variable (pool) before
the try so it is accessible in catch, wrap the client connect/query in a
try-finally to always call client.release(), and in the catch block call await
pool.end() (and reset _postgresState as appropriate) before rethrowing the error
so the Postgres pool is closed on initialization failure; reference
initializePostgresCheckpointer, Pool, client.release(), pool.end(),
PostgresSaver.setup and _postgresState when making these changes.
| export async function healthCheckPostgres(): Promise<{ | ||
| healthy: boolean; | ||
| latency: number; | ||
| connected: boolean; | ||
| }> { | ||
| const start = Date.now(); | ||
|
|
||
| if (!_postgresState.pool) { | ||
| return { healthy: false, latency: Date.now() - start, connected: false }; | ||
| } | ||
|
|
||
| async put( | ||
| threadId: string, | ||
| checkpoint: Checkpoint, | ||
| channels: Record<string, unknown>, | ||
| newChannels: Record<string, unknown>, | ||
| metadata?: Record<string, unknown> | ||
| ): Promise<void> { | ||
| // Placeholder | ||
| try { | ||
| const client = await _postgresState.pool.connect(); | ||
| await client.query('SELECT 1'); | ||
| client.release(); | ||
| const latency = Date.now() - start; | ||
|
|
||
| _postgresState.lastHealthCheck = Date.now(); | ||
|
|
||
| return { healthy: true, latency, connected: _postgresState.isConnected }; | ||
| } catch (error) { | ||
| return { healthy: false, latency: Date.now() - start, connected: false }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
wc -l lib/redis/langgraph-checkpoint.tsRepository: Aparnap2/smart_commerce_agent
Length of output: 110
🏁 Script executed:
sed -n '310,345p' lib/redis/langgraph-checkpoint.tsRepository: Aparnap2/smart_commerce_agent
Length of output: 1028
🏁 Script executed:
# Check initialization of _postgresState and usage of isConnected
rg "_postgresState" lib/redis/langgraph-checkpoint.ts -B 2 -A 2Repository: Aparnap2/smart_commerce_agent
Length of output: 2095
Ensure health checks release Postgres clients and update connection state.
The current code has two issues:
-
Resource leak: If
client.query('SELECT 1')throws, the code jumps to the catch block without releasing the client. Line 327–329 must be wrapped in try-finally to guarantee release. -
Stale state: On health check failure,
_postgresState.isConnectedis not set tofalse, so the connection state may not accurately reflect the actual health. Both success and failure paths should updateisConnectedto keep state consistent.
Proposed fix
try {
const client = await _postgresState.pool.connect();
- await client.query('SELECT 1');
- client.release();
+ try {
+ await client.query('SELECT 1');
+ } finally {
+ client.release();
+ }
const latency = Date.now() - start;
+ _postgresState.isConnected = true;
_postgresState.lastHealthCheck = Date.now();
return { healthy: true, latency, connected: _postgresState.isConnected };
} catch (error) {
+ _postgresState.isConnected = false;
return { healthy: false, latency: Date.now() - start, connected: false };
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function healthCheckPostgres(): Promise<{ | |
| healthy: boolean; | |
| latency: number; | |
| connected: boolean; | |
| }> { | |
| const start = Date.now(); | |
| if (!_postgresState.pool) { | |
| return { healthy: false, latency: Date.now() - start, connected: false }; | |
| } | |
| async put( | |
| threadId: string, | |
| checkpoint: Checkpoint, | |
| channels: Record<string, unknown>, | |
| newChannels: Record<string, unknown>, | |
| metadata?: Record<string, unknown> | |
| ): Promise<void> { | |
| // Placeholder | |
| try { | |
| const client = await _postgresState.pool.connect(); | |
| await client.query('SELECT 1'); | |
| client.release(); | |
| const latency = Date.now() - start; | |
| _postgresState.lastHealthCheck = Date.now(); | |
| return { healthy: true, latency, connected: _postgresState.isConnected }; | |
| } catch (error) { | |
| return { healthy: false, latency: Date.now() - start, connected: false }; | |
| } | |
| export async function healthCheckPostgres(): Promise<{ | |
| healthy: boolean; | |
| latency: number; | |
| connected: boolean; | |
| }> { | |
| const start = Date.now(); | |
| if (!_postgresState.pool) { | |
| return { healthy: false, latency: Date.now() - start, connected: false }; | |
| } | |
| try { | |
| const client = await _postgresState.pool.connect(); | |
| try { | |
| await client.query('SELECT 1'); | |
| } finally { | |
| client.release(); | |
| } | |
| const latency = Date.now() - start; | |
| _postgresState.isConnected = true; | |
| _postgresState.lastHealthCheck = Date.now(); | |
| return { healthy: true, latency, connected: _postgresState.isConnected }; | |
| } catch (error) { | |
| _postgresState.isConnected = false; | |
| return { healthy: false, latency: Date.now() - start, connected: false }; | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@lib/redis/langgraph-checkpoint.ts` around lines 315 - 337, The
healthCheckPostgres function must ensure Postgres clients are always released
and _postgresState.isConnected is updated on both success and failure: after
obtaining a client via _postgresState.pool.connect() use a try/finally around
client.query('SELECT 1') so client.release() is called in the finally block, set
_postgresState.lastHealthCheck and _postgresState.isConnected = true on
successful query, and in the catch block set _postgresState.isConnected = false
before returning the failure object; leave pool.connect() errors untouched (no
client to release).
| db-reset: | ||
| @echo -e "$(YELLOW)WARNING: This will delete all data in the database!$(NC)" | ||
| @read -p "Are you sure? (y/N) " -n 1 -r; echo; if [[ $$REPLY =~ ^[Yy]$$ ]]; then npx prisma migrate reset --force; fi | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if SHELL is already defined in the Makefile
rg -n "^SHELL\s*:=" MakefileRepository: Aparnap2/smart_commerce_agent
Length of output: 55
🏁 Script executed:
#!/bin/bash
# Read the Makefile to see the full context around lines 106-109 and check for any SHELL definitions
head -120 Makefile | tail -30Repository: Aparnap2/smart_commerce_agent
Length of output: 996
🏁 Script executed:
#!/bin/bash
# Search for SHELL definitions anywhere in the Makefile
rg -i "shell" MakefileRepository: Aparnap2/smart_commerce_agent
Length of output: 55
🏁 Script executed:
#!/bin/bash
# Check if test-langgraph.sh references a different container name
rg -n "smart-commerce-ollama\|sca-ollama" scripts/test-langgraph.shRepository: Aparnap2/smart_commerce_agent
Length of output: 55
🏁 Script executed:
#!/bin/bash
# Check if there's a docker-compose or similar config that defines container names
fd -e "docker-compose" -e "compose.yaml" -e "compose.yml" | head -5Repository: Aparnap2/smart_commerce_agent
Length of output: 55
db-reset uses bashisms that fail under /bin/sh. Line 108 relies on read -p, [[ ]], and $$REPLY =~, which are bash-specific. Make defaults to /bin/sh when SHELL is not set. Add SHELL := /bin/bash to the top of the Makefile to ensure consistent execution.
🛠️ Suggested fix
+SHELL := /bin/bash
+
.PHONY: test🤖 Prompt for AI Agents
In `@Makefile` around lines 106 - 109, Add a Makefile-level shell setting to
ensure the db-reset target's bash-specific constructs run reliably: add "SHELL
:= /bin/bash" at the top of the Makefile so the db-reset target (the target
named db-reset that uses read -p, [[ ]], and $$REPLY) executes under bash;
alternatively, if you prefer POSIX portability, replace the bashisms in the
db-reset recipe with POSIX-compliant equivalents (use plain read without -p,
single [ ] for tests, and POSIX pattern matching) and keep the SHELL change only
if you want to enforce bash globally.
| ollama-pull: | ||
| @echo -e "$(BLUE)Pulling Ollama models...$(NC)" | ||
| @docker exec sca-ollama ollama pull qwen2.5-coder:3b | ||
| @docker exec sca-ollama ollama pull nomic-embed-text | ||
| @echo -e "$(GREEN)Ollama models pulled successfully$(NC)" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check docker-compose.yml for Ollama service definition
echo "=== docker-compose.yml: Ollama-related entries ==="
rg -n "ollama" docker-compose.yml
echo ""
echo "=== Checking test-langgraph.sh for container references ==="
rg -n "ollama\|sca-ollama\|smart-commerce-ollama" scripts/test-langgraph.sh || echo "File not found or no matches"
echo ""
echo "=== Verifying Makefile lines 112-116 ==="
sed -n '110,120p' MakefileRepository: Aparnap2/smart_commerce_agent
Length of output: 754
🏁 Script executed:
#!/bin/bash
# Get full docker-compose.yml to see service definitions
echo "=== Full docker-compose.yml ==="
cat docker-compose.yml
echo ""
echo "=== List files in scripts directory ==="
ls -la scripts/ | head -20Repository: Aparnap2/smart_commerce_agent
Length of output: 2262
🏁 Script executed:
#!/bin/bash
# Check test-langgraph.sh for Ollama container references
echo "=== test-langgraph.sh: Ollama/container references ==="
rg -n "ollama\|sca-\|smart-commerce" scripts/test-langgraph.sh | head -30Repository: Aparnap2/smart_commerce_agent
Length of output: 127
🏁 Script executed:
#!/bin/bash
# Read test-langgraph.sh to check its content
cat scripts/test-langgraph.shRepository: Aparnap2/smart_commerce_agent
Length of output: 10285
Container name mismatch in ollama-pull.
Lines 115–116 use sca-ollama, but test-langgraph.sh uses smart-commerce-ollama. Update to match the actual container name:
🔧 Proposed fix
- `@docker` exec sca-ollama ollama pull qwen2.5-coder:3b
- `@docker` exec sca-ollama ollama pull nomic-embed-text
+ `@docker` exec smart-commerce-ollama ollama pull qwen2.5-coder:3b
+ `@docker` exec smart-commerce-ollama ollama pull nomic-embed-text📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ollama-pull: | |
| @echo -e "$(BLUE)Pulling Ollama models...$(NC)" | |
| @docker exec sca-ollama ollama pull qwen2.5-coder:3b | |
| @docker exec sca-ollama ollama pull nomic-embed-text | |
| @echo -e "$(GREEN)Ollama models pulled successfully$(NC)" | |
| ollama-pull: | |
| `@echo` -e "$(BLUE)Pulling Ollama models...$(NC)" | |
| `@docker` exec smart-commerce-ollama ollama pull qwen2.5-coder:3b | |
| `@docker` exec smart-commerce-ollama ollama pull nomic-embed-text | |
| `@echo` -e "$(GREEN)Ollama models pulled successfully$(NC)" |
🤖 Prompt for AI Agents
In `@Makefile` around lines 112 - 116, The Makefile target ollama-pull is using
the wrong container name (sca-ollama); update the two docker exec commands
within the ollama-pull target to use the actual container name
smart-commerce-ollama so they match test-langgraph.sh and the running container
(change occurrences of sca-ollama to smart-commerce-ollama in the ollama-pull
recipe).
| -p ${QDRANT_PORT}:6333 \ | ||
| -p ${QDRANT_PORT}:6334 \ |
There was a problem hiding this comment.
Port mapping error: Both Qdrant ports mapped to the same host port.
Line 153-154 both map to host port ${QDRANT_PORT} (6333), but Qdrant exposes two different container ports (6333 for HTTP, 6334 for gRPC). This will fail or cause unexpected behavior.
Proposed fix
docker run -d \
--name "$name" \
-p ${QDRANT_PORT}:6333 \
- -p ${QDRANT_PORT}:6334 \
+ -p 6334:6334 \
--restart unless-stopped \📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| -p ${QDRANT_PORT}:6333 \ | |
| -p ${QDRANT_PORT}:6334 \ | |
| -p ${QDRANT_PORT}:6333 \ | |
| -p 6334:6334 \ |
🤖 Prompt for AI Agents
In `@scripts/start-infrastructure.sh` around lines 153 - 154, The port mapping
maps both container ports 6333 and 6334 to the same host port variable
QDRANT_PORT; update the second mapping so host and container ports are distinct
(e.g., keep "-p ${QDRANT_PORT}:6333" for HTTP and change the gRPC mapping to "-p
${QDRANT_GRPC_PORT}:6334" or similar), ensure you introduce/use a new host
variable (QDRANT_GRPC_PORT) in the script or environment and update any
references or defaults so 6334 is exposed properly for gRPC.
- Add complete method stubs to no-op client for proper type safety - Fix negative latency bug in fallback scoring - Ensure scoring module returns correct positive latency values 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Update env.js with Neon connection pool configuration - Add Neon detection to langgraph-checkpoint.ts for serverless optimization - Create comprehensive CLOUD_DEPLOYMENT.md guide - Add ADR-009 for cloud-native free tier architecture 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@docs/adr/adr-001-architecture-decisions.md`:
- Around line 490-499: The markdown tables in this ADR (notably the ADR index
table shown and the "Summary", "Revision History", and "ADR-009" tables) violate
MD060 pipe spacing/alignment; fix them by normalizing column padding so each
cell has a single space on both sides of the pipe and the separator row uses
consistent dashes (e.g., "| Col | Col2 |") across all rows; update the ADR index
table (the block listing ADR 001–008) and the other mentioned tables so their
pipes and column widths align consistently to clear the lint warnings.
- Around line 7-15: The Table of Contents stops at ADR-008 but ADR-009 exists
later; update the TOC by adding an entry for ADR-009 to the numbered list so
navigation includes it (match the style used for entries 1–8). Locate the TOC
block at the top of the ADR document and insert a new line like the other items
referencing the ADR anchor (ADR-009) and the human-readable title (e.g.,
"ADR-009: <Title>") ensuring numbering and anchor format follow the existing
pattern.
In `@docs/CLOUD_DEPLOYMENT.md`:
- Around line 62-71: The environment/build/cost markdown tables (e.g., rows
containing DATABASE_URL, CHECKPOINT_TYPE, LANGFUSE_PUBLIC_KEY, OLLAMA_BASE_URL,
OLLAMA_MODEL) use inconsistent pipe spacing and must be realigned to satisfy
MD060; fix each table by aligning the vertical pipes and cell padding so columns
line up vertically (consistent spaces before/after `|` in every row) and apply
the same alignment pattern to the other tables flagged (the ones around the
sections that include rows 76-82, 123-127, and 185-191) so all tables have
uniform pipe spacing.
- Around line 7-20: The fenced code blocks in CLOUD_DEPLOYMENT.md (the ASCII
architecture diagram and the project/details block) are missing language
identifiers and trigger MD040; update each triple-backtick fence that surrounds
the diagram and the project/details section to include a language identifier
such as text (e.g., change ``` to ```text) so both blocks are marked as plain
text; ensure you update both occurrences referenced in the diff (the large ASCII
box and the Project/Database block).
- Line 193: Replace the bolded line "**Total Monthly Cost: $0**" with a proper
Markdown heading to satisfy MD036 (avoid using emphasis as a heading); change
the text "Total Monthly Cost: $0" to a heading (for example, prefix with one or
more # characters like "## Total Monthly Cost: $0" using the appropriate level
for the document) so the line is a real heading instead of bold emphasis.
- Around line 64-71: Update the cloud deployment docs to stop suggesting a
localhost Ollama endpoint: replace the example value for OLLAMA_BASE_URL
(currently "http://localhost:11434") with a reachable hosted endpoint or add a
clear note that OLLAMA_BASE_URL must be changed for cloud deployments (e.g.,
point to Ollama Cloud, Together AI, or a self-hosted URL), and also add
OLLAMA_BASE_URL (and OLLAMA_MODEL) to the Render environment variables section
so the background worker configuration is consistent with the Vercel table.
In `@lib/observability/langfuse.ts`:
- Around line 58-63: The sampleRate assignment currently uses || which treats 0
as falsy and forces a default of 1.0; change the logic in the sampleRate
initializer so that it uses nullish coalescing (??) to respect an explicit 0
from config or env (i.e., check config?.sampleRate then
env.LANGFUSE_SAMPLING_RATE then fallback to 1.0) — update the sampleRate
declaration in this module so 0 is not overridden.
In `@lib/observability/scoring.ts`:
- Around line 127-134: The overallScore calculation currently uses the logical
OR operator which treats valid zero values as falsy and replaces them with 0.7;
update the computation to preserve explicit zeros by using nullish coalescing
(or an undefined check) for each metric (e.g., replace uses of scores.relevance
|| 0.7, scores.accuracy || 0.7, scores.completeness || 0.7, scores.coherence ||
0.7, scores.helpfulness || 0.7 with scores.relevance ?? 0.7, etc.) so that 0
scores from parsed.scores are honored when computing overallScore.
| 1. [ADR-001: LangGraph for Agent Orchestration](#adr-001-langgraph-for-agent-orchestration) | ||
| 2. [ADR-002: Multi-Checkpointer Strategy](#adr-002-multi-checkpointer-strategy) | ||
| 3. [ADR-003: Qdrant for Vector Search](#adr-003-qdrant-for-vector-search) | ||
| 4. [ADR-004: Ollama for Local LLM Inference](#adr-004-ollama-for-local-llm-inference) | ||
| 5. [ADR-005: Langfuse for Observability](#adr-005-langfuse-for-observability) | ||
| 6. [ADR-006: Intent Classification Router](#adr-006-intent-classification-router) | ||
| 7. [ADR-007: Human-in-the-Loop for Refunds](#adr-007-human-in-the-loop-for-refunds) | ||
| 8. [ADR-008: Dual-Mode Scoring System](#adr-008-dual-mode-scoring-system) | ||
|
|
There was a problem hiding this comment.
Add ADR-009 to the Table of Contents.
The TOC stops at ADR-008 but ADR-009 is included later, so navigation is incomplete.
✍️ Suggested fix
7. [ADR-007: Human-in-the-Loop for Refunds](`#adr-007-human-in-the-loop-for-refunds`)
8. [ADR-008: Dual-Mode Scoring System](`#adr-008-dual-mode-scoring-system`)
+9. [ADR-009: Cloud-Native Free Tier Architecture](`#adr-009-cloud-native-free-tier-architecture`)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 1. [ADR-001: LangGraph for Agent Orchestration](#adr-001-langgraph-for-agent-orchestration) | |
| 2. [ADR-002: Multi-Checkpointer Strategy](#adr-002-multi-checkpointer-strategy) | |
| 3. [ADR-003: Qdrant for Vector Search](#adr-003-qdrant-for-vector-search) | |
| 4. [ADR-004: Ollama for Local LLM Inference](#adr-004-ollama-for-local-llm-inference) | |
| 5. [ADR-005: Langfuse for Observability](#adr-005-langfuse-for-observability) | |
| 6. [ADR-006: Intent Classification Router](#adr-006-intent-classification-router) | |
| 7. [ADR-007: Human-in-the-Loop for Refunds](#adr-007-human-in-the-loop-for-refunds) | |
| 8. [ADR-008: Dual-Mode Scoring System](#adr-008-dual-mode-scoring-system) | |
| 1. [ADR-001: LangGraph for Agent Orchestration](`#adr-001-langgraph-for-agent-orchestration`) | |
| 2. [ADR-002: Multi-Checkpointer Strategy](`#adr-002-multi-checkpointer-strategy`) | |
| 3. [ADR-003: Qdrant for Vector Search](`#adr-003-qdrant-for-vector-search`) | |
| 4. [ADR-004: Ollama for Local LLM Inference](`#adr-004-ollama-for-local-llm-inference`) | |
| 5. [ADR-005: Langfuse for Observability](`#adr-005-langfuse-for-observability`) | |
| 6. [ADR-006: Intent Classification Router](`#adr-006-intent-classification-router`) | |
| 7. [ADR-007: Human-in-the-Loop for Refunds](`#adr-007-human-in-the-loop-for-refunds`) | |
| 8. [ADR-008: Dual-Mode Scoring System](`#adr-008-dual-mode-scoring-system`) | |
| 9. [ADR-009: Cloud-Native Free Tier Architecture](`#adr-009-cloud-native-free-tier-architecture`) |
🤖 Prompt for AI Agents
In `@docs/adr/adr-001-architecture-decisions.md` around lines 7 - 15, The Table of
Contents stops at ADR-008 but ADR-009 exists later; update the TOC by adding an
entry for ADR-009 to the numbered list so navigation includes it (match the
style used for entries 1–8). Locate the TOC block at the top of the ADR document
and insert a new line like the other items referencing the ADR anchor (ADR-009)
and the human-readable title (e.g., "ADR-009: <Title>") ensuring numbering and
anchor format follow the existing pattern.
| | ADR | Decision | Key Benefit | | ||
| |-----|----------|-------------| | ||
| | 001 | LangGraph | Explicit workflow control + persistence | | ||
| | 002 | Multi-checkpointer | Environment flexibility | | ||
| | 003 | Qdrant | Semantic product search | | ||
| | 004 | Ollama | Local, cost-free LLM inference | | ||
| | 005 | Langfuse | Complete observability | | ||
| | 006 | Intent Router | Intelligent query routing | | ||
| | 007 | Human-in-loop | Refund approval workflow | | ||
| | 008 | Dual-mode scoring | Reliable quality evaluation | |
There was a problem hiding this comment.
Fix markdown table pipe spacing (MD060).
markdownlint flags the Summary, Revision History, and ADR-009 tables for pipe spacing/alignment. Aligning pipes will clear the lint warnings.
🧹 Example alignment (apply similarly to other tables)
-| ADR | Decision | Key Benefit |
-|-----|----------|-------------|
-| 001 | LangGraph | Explicit workflow control + persistence |
+| ADR | Decision | Key Benefit |
+| --- | --------- | ----------------------------------------- |
+| 001 | LangGraph | Explicit workflow control + persistence |Also applies to: 505-507, 527-533
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)
[warning] 491-491: Table column style
Table pipe is missing space to the right for style "compact"
(MD060, table-column-style)
[warning] 491-491: Table column style
Table pipe is missing space to the left for style "compact"
(MD060, table-column-style)
[warning] 491-491: Table column style
Table pipe is missing space to the right for style "compact"
(MD060, table-column-style)
[warning] 491-491: Table column style
Table pipe is missing space to the left for style "compact"
(MD060, table-column-style)
[warning] 491-491: Table column style
Table pipe is missing space to the right for style "compact"
(MD060, table-column-style)
[warning] 491-491: Table column style
Table pipe is missing space to the left for style "compact"
(MD060, table-column-style)
🤖 Prompt for AI Agents
In `@docs/adr/adr-001-architecture-decisions.md` around lines 490 - 499, The
markdown tables in this ADR (notably the ADR index table shown and the
"Summary", "Revision History", and "ADR-009" tables) violate MD060 pipe
spacing/alignment; fix them by normalizing column padding so each cell has a
single space on both sides of the pipe and the separator row uses consistent
dashes (e.g., "| Col | Col2 |") across all rows; update the ADR index table (the
block listing ADR 001–008) and the other mentioned tables so their pipes and
column widths align consistently to clear the lint warnings.
| ``` | ||
| ┌─────────────────────────────────────────────────────────────┐ | ||
| │ Smart Commerce Agent │ | ||
| │ (LangGraph + Next.js) │ | ||
| ├─────────────────────────────────────────────────────────────┤ | ||
| │ State Store: │ | ||
| │ Neon Postgres (Free Tier) │ | ||
| │ Checkpoint Type: postgres │ | ||
| ├─────────────────────────────────────────────────────────────┤ | ||
| │ Vercel (Frontend/API) │ Render (Worker) │ | ||
| │ - Next.js App │ - LangGraph Agent │ | ||
| │ - Chat UI │ - Background Tasks │ | ||
| └─────────────────────────────────────────────────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
Add language identifiers to fenced blocks (MD040).
The architecture diagram and the project details block should specify a language (e.g., text) to satisfy linting.
🛠️ Suggested change
-```
+```text
┌─────────────────────────────────────────────────────────────┐
│ Smart Commerce Agent │
@@
└─────────────────────────────────────────────────────────────┘
-```
+```
@@
-```
+```text
Project: smart-commerce-agent
Database: smart_commerce
-```
+```Also applies to: 28-31
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)
[warning] 7-7: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In `@docs/CLOUD_DEPLOYMENT.md` around lines 7 - 20, The fenced code blocks in
CLOUD_DEPLOYMENT.md (the ASCII architecture diagram and the project/details
block) are missing language identifiers and trigger MD040; update each
triple-backtick fence that surrounds the diagram and the project/details section
to include a language identifier such as text (e.g., change ``` to ```text) so
both blocks are marked as plain text; ensure you update both occurrences
referenced in the diff (the large ASCII box and the Project/Database block).
| | Variable | Value | Type | | ||
| |----------|-------|------| | ||
| | `DATABASE_URL` | `postgresql://...@ep-xxx.neon.tech/...` | Secret | | ||
| | `CHECKPOINT_TYPE` | `postgres` | Plain | | ||
| | `LANGFUSE_PUBLIC_KEY` | `pl_...` | Secret | | ||
| | `LANGFUSE_SECRET_KEY` | `sk_...` | Secret | | ||
| | `LANGFUSE_BASE_URL` | `https://cloud.langfuse.com` | Plain | | ||
| | `OLLAMA_BASE_URL` | `http://localhost:11434` | Plain | | ||
| | `OLLAMA_MODEL` | `qwen2.5-coder:3b` | Plain | | ||
|
|
There was a problem hiding this comment.
Fix table pipe spacing across environment/build/cost tables (MD060).
Multiple tables are flagged for compact pipe spacing. Aligning pipes will satisfy linting.
🧹 Example alignment (apply to all tables)
-| Variable | Value | Type |
-|----------|-------|------|
+| Variable | Value | Type |
+| ----------------- | -------------------------------------- | ------ |Also applies to: 76-82, 123-127, 185-191
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)
[warning] 63-63: Table column style
Table pipe is missing space to the right for style "compact"
(MD060, table-column-style)
[warning] 63-63: Table column style
Table pipe is missing space to the left for style "compact"
(MD060, table-column-style)
[warning] 63-63: Table column style
Table pipe is missing space to the right for style "compact"
(MD060, table-column-style)
[warning] 63-63: Table column style
Table pipe is missing space to the left for style "compact"
(MD060, table-column-style)
[warning] 63-63: Table column style
Table pipe is missing space to the right for style "compact"
(MD060, table-column-style)
[warning] 63-63: Table column style
Table pipe is missing space to the left for style "compact"
(MD060, table-column-style)
🤖 Prompt for AI Agents
In `@docs/CLOUD_DEPLOYMENT.md` around lines 62 - 71, The environment/build/cost
markdown tables (e.g., rows containing DATABASE_URL, CHECKPOINT_TYPE,
LANGFUSE_PUBLIC_KEY, OLLAMA_BASE_URL, OLLAMA_MODEL) use inconsistent pipe
spacing and must be realigned to satisfy MD060; fix each table by aligning the
vertical pipes and cell padding so columns line up vertically (consistent spaces
before/after `|` in every row) and apply the same alignment pattern to the other
tables flagged (the ones around the sections that include rows 76-82, 123-127,
and 185-191) so all tables have uniform pipe spacing.
| | `DATABASE_URL` | `postgresql://...@ep-xxx.neon.tech/...` | Secret | | ||
| | `CHECKPOINT_TYPE` | `postgres` | Plain | | ||
| | `LANGFUSE_PUBLIC_KEY` | `pl_...` | Secret | | ||
| | `LANGFUSE_SECRET_KEY` | `sk_...` | Secret | | ||
| | `LANGFUSE_BASE_URL` | `https://cloud.langfuse.com` | Plain | | ||
| | `OLLAMA_BASE_URL` | `http://localhost:11434` | Plain | | ||
| | `OLLAMA_MODEL` | `qwen2.5-coder:3b` | Plain | | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n docs/CLOUD_DEPLOYMENT.md | head -100Repository: Aparnap2/smart_commerce_agent
Length of output: 3729
🏁 Script executed:
cat -n docs/CLOUD_DEPLOYMENT.md | sed -n '56,150p'Repository: Aparnap2/smart_commerce_agent
Length of output: 2968
🏁 Script executed:
cat -n docs/CLOUD_DEPLOYMENT.md | sed -n '150,250p'Repository: Aparnap2/smart_commerce_agent
Length of output: 1646
🏁 Script executed:
# Check if there's any explanation about optional variables or OLLAMA configuration
rg -A 5 -B 5 "OLLAMA" docs/Repository: Aparnap2/smart_commerce_agent
Length of output: 7580
Update OLLAMA_BASE_URL for cloud environments.
The table at lines 64-70 configures OLLAMA_BASE_URL to http://localhost:11434 for Vercel cloud deployment, but this endpoint is unreachable in cloud environments. Either update the example to point to a reachable hosted OLLAMA endpoint (e.g., Ollama Cloud, Together AI, or self-hosted), or clearly document that this variable requires modification for cloud deployment. Note: The Render environment variables table (lines 76-82) omits OLLAMA entirely, creating inconsistency about how to configure it for background workers.
🤖 Prompt for AI Agents
In `@docs/CLOUD_DEPLOYMENT.md` around lines 64 - 71, Update the cloud deployment
docs to stop suggesting a localhost Ollama endpoint: replace the example value
for OLLAMA_BASE_URL (currently "http://localhost:11434") with a reachable hosted
endpoint or add a clear note that OLLAMA_BASE_URL must be changed for cloud
deployments (e.g., point to Ollama Cloud, Together AI, or a self-hosted URL),
and also add OLLAMA_BASE_URL (and OLLAMA_MODEL) to the Render environment
variables section so the background worker configuration is consistent with the
Vercel table.
| | Langfuse | 50,000 traces/month | $0 | | ||
| | Qdrant Cloud | 1 cluster, 1GB storage | $0 | | ||
|
|
||
| **Total Monthly Cost: $0** |
There was a problem hiding this comment.
Use a heading instead of bold text for the total (MD036).
markdownlint flags emphasis used as a heading.
✍️ Suggested fix
-**Total Monthly Cost: $0**
+### Total Monthly Cost: $0📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| **Total Monthly Cost: $0** | |
| ### Total Monthly Cost: $0 |
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)
[warning] 193-193: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🤖 Prompt for AI Agents
In `@docs/CLOUD_DEPLOYMENT.md` at line 193, Replace the bolded line "**Total
Monthly Cost: $0**" with a proper Markdown heading to satisfy MD036 (avoid using
emphasis as a heading); change the text "Total Monthly Cost: $0" to a heading
(for example, prefix with one or more # characters like "## Total Monthly Cost:
$0" using the appropriate level for the document) so the line is a real heading
instead of bold emphasis.
| const publicKey = config?.publicKey || env.LANGFUSE_PUBLIC_KEY || ''; | ||
| const secretKey = config?.secretKey || env.LANGFUSE_SECRET_KEY || ''; | ||
| const baseUrl = config?.baseUrl || env.LANGFUSE_BASE_URL || 'https://cloud.langfuse.com'; | ||
| const environment = config?.environment || env.LANGFUSE_ENVIRONMENT || 'development'; | ||
| const sampleRate = (config?.sampleRate ?? env.LANGFUSE_SAMPLING_RATE) || 1.0; | ||
|
|
There was a problem hiding this comment.
Allow sampleRate = 0 to disable tracing.
Using || 1.0 overrides an explicit 0. Use nullish coalescing so 0 is respected.
🛠️ Suggested fix
- const sampleRate = (config?.sampleRate ?? env.LANGFUSE_SAMPLING_RATE) || 1.0;
+ const sampleRate = config?.sampleRate ?? env.LANGFUSE_SAMPLING_RATE ?? 1.0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const publicKey = config?.publicKey || env.LANGFUSE_PUBLIC_KEY || ''; | |
| const secretKey = config?.secretKey || env.LANGFUSE_SECRET_KEY || ''; | |
| const baseUrl = config?.baseUrl || env.LANGFUSE_BASE_URL || 'https://cloud.langfuse.com'; | |
| const environment = config?.environment || env.LANGFUSE_ENVIRONMENT || 'development'; | |
| const sampleRate = (config?.sampleRate ?? env.LANGFUSE_SAMPLING_RATE) || 1.0; | |
| const publicKey = config?.publicKey || env.LANGFUSE_PUBLIC_KEY || ''; | |
| const secretKey = config?.secretKey || env.LANGFUSE_SECRET_KEY || ''; | |
| const baseUrl = config?.baseUrl || env.LANGFUSE_BASE_URL || 'https://cloud.langfuse.com'; | |
| const environment = config?.environment || env.LANGFUSE_ENVIRONMENT || 'development'; | |
| const sampleRate = config?.sampleRate ?? env.LANGFUSE_SAMPLING_RATE ?? 1.0; |
🤖 Prompt for AI Agents
In `@lib/observability/langfuse.ts` around lines 58 - 63, The sampleRate
assignment currently uses || which treats 0 as falsy and forces a default of
1.0; change the logic in the sampleRate initializer so that it uses nullish
coalescing (??) to respect an explicit 0 from config or env (i.e., check
config?.sampleRate then env.LANGFUSE_SAMPLING_RATE then fallback to 1.0) —
update the sampleRate declaration in this module so 0 is not overridden.
| const scores = parsed.scores || {}; | ||
| const overallScore = ( | ||
| (scores.relevance || 0.7) + | ||
| (scores.accuracy || 0.7) + | ||
| (scores.completeness || 0.7) + | ||
| (scores.coherence || 0.7) + | ||
| (scores.helpfulness || 0.7) | ||
| ) / 5; |
There was a problem hiding this comment.
Preserve zero scores in overallScore calculation.
Using || overwrites valid 0 scores with 0.7, inflating results.
🛠️ Suggested fix
- const overallScore = (
- (scores.relevance || 0.7) +
- (scores.accuracy || 0.7) +
- (scores.completeness || 0.7) +
- (scores.coherence || 0.7) +
- (scores.helpfulness || 0.7)
- ) / 5;
+ const overallScore = (
+ (scores.relevance ?? 0.7) +
+ (scores.accuracy ?? 0.7) +
+ (scores.completeness ?? 0.7) +
+ (scores.coherence ?? 0.7) +
+ (scores.helpfulness ?? 0.7)
+ ) / 5;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const scores = parsed.scores || {}; | |
| const overallScore = ( | |
| (scores.relevance || 0.7) + | |
| (scores.accuracy || 0.7) + | |
| (scores.completeness || 0.7) + | |
| (scores.coherence || 0.7) + | |
| (scores.helpfulness || 0.7) | |
| ) / 5; | |
| const scores = parsed.scores || {}; | |
| const overallScore = ( | |
| (scores.relevance ?? 0.7) + | |
| (scores.accuracy ?? 0.7) + | |
| (scores.completeness ?? 0.7) + | |
| (scores.coherence ?? 0.7) + | |
| (scores.helpfulness ?? 0.7) | |
| ) / 5; |
🤖 Prompt for AI Agents
In `@lib/observability/scoring.ts` around lines 127 - 134, The overallScore
calculation currently uses the logical OR operator which treats valid zero
values as falsy and replaces them with 0.7; update the computation to preserve
explicit zeros by using nullish coalescing (or an undefined check) for each
metric (e.g., replace uses of scores.relevance || 0.7, scores.accuracy || 0.7,
scores.completeness || 0.7, scores.coherence || 0.7, scores.helpfulness || 0.7
with scores.relevance ?? 0.7, etc.) so that 0 scores from parsed.scores are
honored when computing overallScore.
- Add lib/llm/provider.ts for unified LLM interface - Primary: OpenAI API (production/serverless) - Fallback: Ollama (local development) - Update supervisor.ts to use abstraction layer - Add test scripts for LLM validation - Fix TypeScript types and imports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@lib/agents/supervisor.ts`:
- Around line 408-417: The catch block that returns a fallback AI message should
also log the caught error using the same logging mechanism as processToolResults
to maintain consistency; update the catch in supervisor.ts (the block that
currently returns messages with id: crypto.randomUUID()) to call the same error
logger used in processToolResults (e.g., processLogger.error or the module's
logger) with the error object and a short context string before returning the
fallback message so errors are recorded for debugging.
- Around line 204-206: The LLM JSON may parse to a value that's not the expected
object shape, so after JSON.parse(response.content) validate that parsed is a
plain object (parsed && typeof parsed === 'object' && !Array.isArray(parsed)),
then safely extract intent and confidence: set intent = typeof parsed.intent ===
'string' ? parsed.intent : 'general_support' and confidence = typeof
parsed.confidence === 'number' ? Math.max(0, Math.min(1, parsed.confidence)) :
0.5; ensure these checks are applied where parsed, intent, and confidence are
defined so downstream code always gets well-typed values.
In `@lib/llm/provider.ts`:
- Around line 159-164: The returned OpenAI branch currently returns data.usage
(snake_case) directly but consumers expect camelCase; in the block where
config.provider === 'openai' (in lib/llm/provider.ts) transform
data.usage.{prompt_tokens,completion_tokens,total_tokens} into {promptTokens,
completionTokens, totalTokens} (with safe fallbacks if data.usage is missing)
before returning so callers of the function receive the expected camelCase usage
shape.
- Around line 146-150: The request URL for Ollama is missing the /v1 prefix,
causing 404s; update the fetch invocation that posts the chat completion (the
fetch call building `${config.baseUrl}/chat/completions` in lib/llm/provider.ts)
to use the OpenAI-compatible path `${config.baseUrl}/v1/chat/completions` so the
request targets /v1/chat/completions; keep the same method, headers, and body
(JSON.stringify(payload)) when making this change.
In `@scripts/test-llm-direct.mjs`:
- Line 54: The test script currently sets format: { type: 'json_object' } which
can mismatch Ollama's expected format used in the provider abstraction (see
provider.ts where Ollama expects format: 'json'); update
scripts/test-llm-direct.mjs to use the same format as the provider (switch to
format: 'json' or otherwise serialize to the exact JSON shape Ollama expects) or
add a conditional to select format based on the provider, ensuring consistency
with the Ollama handling in lib/llm/provider.ts.
- Around line 58-63: The JSON-mode failure branch currently only logs the error
but doesn't flip the test result flag; update the conditional handling around
jsonResponse (the non-OK branch) to set allPassed = false when jsonResponse.ok
is false, and ensure the let allPassed = true declaration is moved up so it's in
scope before the JSON mode test; locate the jsonResponse handling block and the
allPassed variable to make these two changes (set allPassed = false on JSON
failure and declare allPassed earlier).
In `@scripts/test-llm-provider.mjs`:
- Around line 41-43: The token-usage log is reading camelCase properties that
don't exist because the provider returns snake_case fields; update the logging
in scripts/test-llm-provider.mjs to read response.usage.prompt_tokens,
response.usage.completion_tokens and response.usage.total_tokens (or normalize
response.usage to camelCase before logging) so the Tokens line shows actual
values rather than undefined.
🧹 Nitpick comments (13)
lib/env.js (1)
51-63: LGTM with optional suggestions.The new configuration variables for checkpointing, Neon pool, and Langfuse observability have sensible defaults and are well-documented.
Optional improvements for robustness:
LANGFUSE_SAMPLING_RATE(line 63):parseFloatreturnsNaNfor non-numeric input. Consider clamping to valid range.CHECKPOINT_TYPE(line 52): Could validate against allowed values.🔧 Optional: Add input validation
// Checkpointer Configuration - CHECKPOINT_TYPE: process.env.CHECKPOINT_TYPE || 'memory', // 'redis' | 'postgres' | 'memory' + CHECKPOINT_TYPE: ['redis', 'postgres', 'memory'].includes(process.env.CHECKPOINT_TYPE) + ? process.env.CHECKPOINT_TYPE + : 'memory', REDIS_URL: process.env.REDIS_URL || 'redis://localhost:6379', // Neon Connection Pool (for serverless environments) NEON_POOL_MAX: parseInt(process.env.NEON_POOL_MAX || '10', 10), NEON_POOL_MIN: parseInt(process.env.NEON_POOL_MIN || '0', 10), NEON_IDLE_TIMEOUT: parseInt(process.env.NEON_IDLE_TIMEOUT || '30000', 10), // Langfuse Observability Configuration LANGFUSE_PUBLIC_KEY: process.env.LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY: process.env.LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL: process.env.LANGFUSE_BASE_URL || 'https://cloud.langfuse.com', LANGFUSE_ENVIRONMENT: process.env.LANGFUSE_ENVIRONMENT || process.env.NODE_ENV || 'development', - LANGFUSE_SAMPLING_RATE: parseFloat(process.env.LANGFUSE_SAMPLING_RATE || '1.0'), + LANGFUSE_SAMPLING_RATE: Math.min(1, Math.max(0, parseFloat(process.env.LANGFUSE_SAMPLING_RATE || '1.0') || 1.0)),lib/agents/supervisor.ts (6)
76-80: Consider typingpendingToolCallsmore strictly.Using
any[]loses type safety. LangGraph tool calls have a defined structure. Consider using a more specific type to catch issues at compile time.+interface ToolCall { + id: string; + type: 'function'; + function: { name: string; arguments: string }; +} + // Pending tool calls (for ToolNode) -pendingToolCalls: Annotation<any[]>({ +pendingToolCalls: Annotation<ToolCall[]>({ reducer: (prev, next) => [...(prev || []), ...(next || [])], default: () => [], }),
160-163: Type cast bypasses tool type validation.The
as anycast at line 162 silences type errors but may hide incompatibilities between tool definitions and whatToolNodeexpects. This is a known friction point with LangGraph's TypeScript types.Consider adding a comment explaining why the cast is needed, or verify this works correctly at runtime.
329-332: Consider truncating large tool results.If tool results are large (e.g., many product search results),
JSON.stringify(toolResults, null, 2)could produce a string that exceeds the LLM's context window or significantly increases token costs. Consider truncating or summarizing results beyond a reasonable limit.
443-446: Redundant union type in return signature.The return type
'generate_response' | 'generate_response'is equivalent to just'generate_response'. If this is a placeholder for future conditional routing, consider using a comment or TODO. Otherwise, simplify the type.-function shouldContinueAfterTools(state: typeof StateAnnotation.State): 'generate_response' | 'generate_response' { +function shouldContinueAfterTools(state: typeof StateAnnotation.State): 'generate_response' { // After tools execute, always generate response return 'generate_response'; }
474-506: Excessiveas anycasts reduce type safety.Multiple
(workflow as any)casts (lines 475, 478, 489, 490, 499-501, 503) completely bypass TypeScript's type checking for the graph construction. This is a common workaround for LangGraph's TypeScript types but makes the code fragile to refactoring and hides potential errors.Consider:
- Adding a comment explaining why casts are needed
- Creating a typed wrapper if this pattern is used elsewhere
- Filing an issue upstream if types are incorrect
522-526: Graph is recreated on every invocation.
createSupervisorGraphis called on everyrunSupervisorinvocation. Graph compilation can be expensive. Consider caching the compiled graph and reusing it across calls (the checkpointer handles per-thread state).Proposed optimization
+let cachedGraph: Awaited<ReturnType<typeof createSupervisorGraph>> | null = null; +let cachedCheckpointerType: string | null = null; + export async function runSupervisor( input: { message: string; threadId: string; userId: string }, checkpointer?: AnyCheckpointer | undefined ): Promise<typeof StateAnnotation.State> { const cp = checkpointer || await createCheckpointer(); - console.log(`[Supervisor] 🔧 Checkpointer type: ${cp.constructor.name}`); - const graph = await createSupervisorGraph(cp); + // Reuse cached graph if checkpointer type matches + if (!cachedGraph || cachedCheckpointerType !== cp.constructor.name) { + cachedGraph = await createSupervisorGraph(cp); + cachedCheckpointerType = cp.constructor.name; + } + const graph = cachedGraph;docs/ARCHITECTURE.md (1)
40-83: Minor markdown formatting: add language identifiers to ASCII diagram code blocks.The static analysis tool flags several fenced code blocks (lines 40, 393, 507, 519, 552) that lack language specifiers. For ASCII art diagrams, you can use
textorplaintextas the language identifier to satisfy linters while preserving formatting.Additionally, tables at lines 26, 589, 598, 606, 614, 621 should be surrounded by blank lines per MD058.
Example fix for line 40
-``` +```text ┌─────────────────────────────────────────────────────────────────────────────┐scripts/test-llm-direct.mjs (1)
20-31: Consider adding a timeout to fetch calls.The fetch calls have no timeout, which could cause the test script to hang indefinitely if Ollama is unresponsive. For a test script, adding
AbortSignal.timeout()would improve reliability.Example with timeout
const response = await fetch(`${OLLAMA_BASE_URL}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: MODEL, messages: [ { role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: 'What is 2+2?' } ], temperature: 0.3, }), + signal: AbortSignal.timeout(30000), });lib/llm/provider.ts (2)
63-82: Silent fallback to Ollama differs from existinglib/ai/config.jsbehavior.The existing
lib/ai/config.jsthrows an error when no LLM provider is configured, providing clear feedback. This new implementation silently falls back to localhost Ollama, which could mask misconfiguration in production environments where neitherOPENAI_API_KEYnor a running Ollama instance exists.Consider adding a warning log when falling back, or aligning with the existing pattern that throws on missing configuration.
108-108: Fetch calls lack timeout configuration.Network calls without timeouts can hang indefinitely. Consider adding
AbortSignal.timeout()for resilience, especially for the availability check which is likely called at startup.scripts/test-llm-provider.mjs (2)
68-72: Wrapper function can be simplified by importing at the top.The
generateEmbeddingwrapper uses dynamic import to call the provider's function, but since other functions are already imported statically from the same module, this could be simplified.Suggested simplification
-import { createChatCompletion, checkLLMAvailability, getLLMProviderInfo } from '../lib/llm/provider.ts'; +import { createChatCompletion, checkLLMAvailability, getLLMProviderInfo, generateEmbedding } from '../lib/llm/provider.ts';Then remove lines 68-72 and use
generateEmbeddingdirectly at line 60.
57-62: Embeddings test could also run for Ollama provider.The provider abstraction supports Ollama embeddings via
/api/embeddings, so this test could be extended to cover both providers rather than skipping for Ollama.
| const parsed = JSON.parse(response.content); | ||
| const intent = parsed.intent || 'general_support'; | ||
| const confidence = parsed.confidence || 0.5; |
There was a problem hiding this comment.
Validate LLM JSON response structure.
JSON.parse can succeed but return an object missing expected fields (e.g., if the LLM returns {}). The fallback || 'general_support' handles missing intent, but if parsed is not an object (e.g., array or string), property access may behave unexpectedly.
Proposed validation
const parsed = JSON.parse(response.content);
- const intent = parsed.intent || 'general_support';
- const confidence = parsed.confidence || 0.5;
+ const intent = (typeof parsed === 'object' && parsed !== null && typeof parsed.intent === 'string')
+ ? parsed.intent
+ : 'general_support';
+ const confidence = (typeof parsed === 'object' && parsed !== null && typeof parsed.confidence === 'number')
+ ? parsed.confidence
+ : 0.5;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parsed = JSON.parse(response.content); | |
| const intent = parsed.intent || 'general_support'; | |
| const confidence = parsed.confidence || 0.5; | |
| const parsed = JSON.parse(response.content); | |
| const intent = (typeof parsed === 'object' && parsed !== null && typeof parsed.intent === 'string') | |
| ? parsed.intent | |
| : 'general_support'; | |
| const confidence = (typeof parsed === 'object' && parsed !== null && typeof parsed.confidence === 'number') | |
| ? parsed.confidence | |
| : 0.5; |
🤖 Prompt for AI Agents
In `@lib/agents/supervisor.ts` around lines 204 - 206, The LLM JSON may parse to a
value that's not the expected object shape, so after
JSON.parse(response.content) validate that parsed is a plain object (parsed &&
typeof parsed === 'object' && !Array.isArray(parsed)), then safely extract
intent and confidence: set intent = typeof parsed.intent === 'string' ?
parsed.intent : 'general_support' and confidence = typeof parsed.confidence ===
'number' ? Math.max(0, Math.min(1, parsed.confidence)) : 0.5; ensure these
checks are applied where parsed, intent, and confidence are defined so
downstream code always gets well-typed values.
| } catch (error) { | ||
| return { | ||
| messages: [{ | ||
| id: crypto.randomUUID(), | ||
| role: 'ai', | ||
| content: 'How can I help you today?', | ||
| timestamp: Date.now(), | ||
| }], | ||
| }; | ||
| } |
There was a problem hiding this comment.
Silent error handling inconsistent with other nodes.
The catch block returns a fallback message without logging the error, unlike processToolResults (line 363) which logs errors. This inconsistency makes debugging harder.
} catch (error) {
+ console.error('[UIAgent] ❌ Direct response generation failed:', error);
return {
messages: [{📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (error) { | |
| return { | |
| messages: [{ | |
| id: crypto.randomUUID(), | |
| role: 'ai', | |
| content: 'How can I help you today?', | |
| timestamp: Date.now(), | |
| }], | |
| }; | |
| } | |
| } catch (error) { | |
| console.error('[UIAgent] ❌ Direct response generation failed:', error); | |
| return { | |
| messages: [{ | |
| id: crypto.randomUUID(), | |
| role: 'ai', | |
| content: 'How can I help you today?', | |
| timestamp: Date.now(), | |
| }], | |
| }; | |
| } |
🤖 Prompt for AI Agents
In `@lib/agents/supervisor.ts` around lines 408 - 417, The catch block that
returns a fallback AI message should also log the caught error using the same
logging mechanism as processToolResults to maintain consistency; update the
catch in supervisor.ts (the block that currently returns messages with id:
crypto.randomUUID()) to call the same error logger used in processToolResults
(e.g., processLogger.error or the module's logger) with the error object and a
short context string before returning the fallback message so errors are
recorded for debugging.
| const response = await fetch(`${config.baseUrl}/chat/completions`, { | ||
| method: 'POST', | ||
| headers, | ||
| body: JSON.stringify(payload), | ||
| }); |
There was a problem hiding this comment.
Incorrect Ollama endpoint path - missing /v1 prefix.
The code calls ${config.baseUrl}/chat/completions, but Ollama's OpenAI-compatible API is at /v1/chat/completions. For Ollama with baseUrl http://localhost:11434, this results in a 404.
Suggested fix
- const response = await fetch(`${config.baseUrl}/chat/completions`, {
+ const endpoint = config.provider === 'openai'
+ ? `${config.baseUrl}/chat/completions`
+ : `${config.baseUrl}/v1/chat/completions`;
+
+ const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(payload),
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const response = await fetch(`${config.baseUrl}/chat/completions`, { | |
| method: 'POST', | |
| headers, | |
| body: JSON.stringify(payload), | |
| }); | |
| const endpoint = config.provider === 'openai' | |
| ? `${config.baseUrl}/chat/completions` | |
| : `${config.baseUrl}/v1/chat/completions`; | |
| const response = await fetch(endpoint, { | |
| method: 'POST', | |
| headers, | |
| body: JSON.stringify(payload), | |
| }); |
🤖 Prompt for AI Agents
In `@lib/llm/provider.ts` around lines 146 - 150, The request URL for Ollama is
missing the /v1 prefix, causing 404s; update the fetch invocation that posts the
chat completion (the fetch call building `${config.baseUrl}/chat/completions` in
lib/llm/provider.ts) to use the OpenAI-compatible path
`${config.baseUrl}/v1/chat/completions` so the request targets
/v1/chat/completions; keep the same method, headers, and body
(JSON.stringify(payload)) when making this change.
| if (config.provider === 'openai') { | ||
| return { | ||
| content: data.choices?.[0]?.message?.content || '', | ||
| usage: data.usage, | ||
| }; | ||
| } |
There was a problem hiding this comment.
OpenAI usage field has snake_case keys, but interface expects camelCase.
The OpenAI API returns { prompt_tokens, completion_tokens, total_tokens }, but ChatCompletionResponse.usage expects { promptTokens, completionTokens, totalTokens }. This will cause consumers to receive undefined when accessing the camelCase properties.
Suggested fix
if (config.provider === 'openai') {
return {
content: data.choices?.[0]?.message?.content || '',
- usage: data.usage,
+ usage: data.usage ? {
+ promptTokens: data.usage.prompt_tokens,
+ completionTokens: data.usage.completion_tokens,
+ totalTokens: data.usage.total_tokens,
+ } : undefined,
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (config.provider === 'openai') { | |
| return { | |
| content: data.choices?.[0]?.message?.content || '', | |
| usage: data.usage, | |
| }; | |
| } | |
| if (config.provider === 'openai') { | |
| return { | |
| content: data.choices?.[0]?.message?.content || '', | |
| usage: data.usage ? { | |
| promptTokens: data.usage.prompt_tokens, | |
| completionTokens: data.usage.completion_tokens, | |
| totalTokens: data.usage.total_tokens, | |
| } : undefined, | |
| }; | |
| } |
🤖 Prompt for AI Agents
In `@lib/llm/provider.ts` around lines 159 - 164, The returned OpenAI branch
currently returns data.usage (snake_case) directly but consumers expect
camelCase; in the block where config.provider === 'openai' (in
lib/llm/provider.ts) transform
data.usage.{prompt_tokens,completion_tokens,total_tokens} into {promptTokens,
completionTokens, totalTokens} (with safe fallbacks if data.usage is missing)
before returning so callers of the function receive the expected camelCase usage
shape.
| { role: 'user', content: 'Return a JSON object with keys "name" and "age"' } | ||
| ], | ||
| temperature: 0.2, | ||
| format: { type: 'json_object' }, |
There was a problem hiding this comment.
JSON mode format may not work with Ollama's OpenAI-compatible endpoint.
The payload uses format: { type: 'json_object' }, but the provider abstraction in lib/llm/provider.ts (line 204) uses format: 'json' for Ollama. The /v1/chat/completions endpoint behavior depends on Ollama's version and configuration.
Consider aligning with the provider abstraction or verifying Ollama's expected format.
Suggested fix to match provider abstraction
- format: { type: 'json_object' },
+ format: 'json',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| format: { type: 'json_object' }, | |
| format: 'json', |
🤖 Prompt for AI Agents
In `@scripts/test-llm-direct.mjs` at line 54, The test script currently sets
format: { type: 'json_object' } which can mismatch Ollama's expected format used
in the provider abstraction (see provider.ts where Ollama expects format:
'json'); update scripts/test-llm-direct.mjs to use the same format as the
provider (switch to format: 'json' or otherwise serialize to the exact JSON
shape Ollama expects) or add a conditional to select format based on the
provider, ensuring consistency with the Ollama handling in lib/llm/provider.ts.
| if (!jsonResponse.ok) { | ||
| console.log(` JSON mode failed: HTTP ${jsonResponse.status}`); | ||
| } else { | ||
| const jsonData = await jsonResponse.json(); | ||
| console.log(` Response: ${jsonData.choices?.[0]?.message?.content}`); | ||
| } |
There was a problem hiding this comment.
JSON mode failure is not tracked in allPassed.
When JSON mode returns a non-OK response, the script logs the failure but does not set allPassed = false, so the final summary may incorrectly report "All Ollama tests passed!" even though JSON mode failed.
Suggested fix
if (!jsonResponse.ok) {
console.log(` JSON mode failed: HTTP ${jsonResponse.status}`);
+ allPassed = false;
} else {Note: You'll also need to move the let allPassed = true; declaration before line 44 (the JSON mode test) for this to work correctly.
🤖 Prompt for AI Agents
In `@scripts/test-llm-direct.mjs` around lines 58 - 63, The JSON-mode failure
branch currently only logs the error but doesn't flip the test result flag;
update the conditional handling around jsonResponse (the non-OK branch) to set
allPassed = false when jsonResponse.ok is false, and ensure the let allPassed =
true declaration is moved up so it's in scope before the JSON mode test; locate
the jsonResponse handling block and the allPassed variable to make these two
changes (set allPassed = false on JSON failure and declare allPassed earlier).
| if (response.usage) { | ||
| console.log(` Tokens: ${response.usage.promptTokens} + ${response.usage.completionTokens} = ${response.usage.totalTokens}`); | ||
| } |
There was a problem hiding this comment.
Token usage logging will show undefined due to upstream type mismatch.
The provider.ts returns OpenAI's snake_case usage fields directly, but this code accesses camelCase properties (promptTokens, completionTokens, totalTokens). Once the upstream issue in lib/llm/provider.ts is fixed, this will work correctly.
🤖 Prompt for AI Agents
In `@scripts/test-llm-provider.mjs` around lines 41 - 43, The token-usage log is
reading camelCase properties that don't exist because the provider returns
snake_case fields; update the logging in scripts/test-llm-provider.mjs to read
response.usage.prompt_tokens, response.usage.completion_tokens and
response.usage.total_tokens (or normalize response.usage to camelCase before
logging) so the Tokens line shows actual values rather than undefined.
- Add Supabase SDK with typed client (auth, realtime, CRUD) - Create schema with RLS policies for tenant isolation - Implement auth pages (login, signup, forgot-password) - Build chat service for realtime ticket messaging - Create MCP adapter using Supabase SDK instead of Prisma - Add LangGraph tools backed by Supabase - Write 20 integration tests (all passing) - Add middleware for route protection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/env.js (1)
18-66:⚠️ Potential issue | 🟡 MinorAdd
server-onlyguard to prevent accidental client-side secret exposure.The module exports server secrets (
STRIPE_SECRET_KEY,LANGFUSE_SECRET_KEY,SUPABASE_SERVICE_ROLE_KEY,REDIS_PASSWORD) without aserver-onlyguard. While all current imports are server-side, this protective measure should be enforced to prevent future accidental bundling to the client.Suggested fix
+import 'server-only'; + /** * Environment variable validationpackage.json (1)
6-11:⚠️ Potential issue | 🟡 MinorAdd a
testscript to make Vitest executable.Vitest was added (vitest.config.ts) but the root package.json lacks a
testscript. This causespnpm testcalled by Makefile:90 to fail. Wire it tovitest runand optionally add a watch variant:Suggested script additions
"scripts": { "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test": "vitest run", + "test:watch": "vitest" },
🤖 Fix all issues with AI agents
In `@app/auth/callback/route.ts`:
- Around line 17-63: The redirect uses the user-controlled variable next and
directly concatenates it into NextResponse.redirect, allowing open-redirects;
validate and normalize next before redirecting by ensuring it is a safe relative
path (e.g., non-empty, starts with a single '/' but not '//' and contains no
scheme or '@'), fallback to '/dashboard' if invalid, and build the final
redirect using new URL(validatedNext, origin) (use origin and
NextResponse.redirect(newUrl.toString())) so the target is guaranteed
same-origin; reference the next variable, origin, request.url, and the
NextResponse.redirect call in your change.
In `@components/auth-provider.tsx`:
- Around line 68-86: The signOut and refreshSession functions create new
Supabase clients causing subscription mismatch and refreshSession only reads the
session instead of refreshing it; fix by reusing the same client instance
created in the useEffect (e.g., store it in a ref like supabaseRef.current when
you call createClient() in useEffect) and update signOut and refreshSession to
use that ref instead of calling createClient(); also change refreshSession to
call supabase.auth.refreshSession() (and then setSession/setUser from the
returned session) rather than auth.getSession().
In `@lib/agents/supabase-tools.ts`:
- Around line 233-238: The order lookup currently only filters by email; update
the where clause used in getDb().orders.findMany inside the orderLookup logic to
build a dynamic filter object that includes customer_email when input.email is
present, id/order_id when input.orderId is present, and status when input.status
is present (only add each key if its corresponding input is provided), then pass
that composed where object to db.orders.findMany along with take: input.limit;
use getDb() and db.orders.findMany as the insertion points to locate and replace
the existing hardcoded where: input.email ? { customer_email: input.email } : {}
logic.
- Around line 275-291: The code always inserts a new refund via getDb() ->
db.refunds.create and ignores input.idempotencyKey, so retries can create
duplicates; fix by adding an idempotency guard: before calling db.refunds.create
check for an existing record using the idempotency key (e.g., query
db.refunds.findFirst or equivalent filtering on idempotency_key ===
input.idempotencyKey and optionally order_id === input.orderId) and return that
refund if found; alternatively, enforce uniqueness at the DB/schema level by
marking idempotency_key as unique in your ORM/schema (e.g., add `@unique` to
idempotency_key and create a migration) and update the create flow to handle
unique-constraint violations (catch the error, re-query by idempotency_key and
return the existing refund) while keeping refundNumber and other logic intact.
In `@lib/supabase/client.ts`:
- Around line 303-321: The update function (exported as update) is calling
.single() without .select(), so Supabase returns null; update the call chain on
the Supabase client (the expression starting from
client.from(table).update(...)) to request the updated row by adding
.select('*') (or the explicit column list) before .single(), ensuring the
function returns the updated record; no other behavior changes needed (keep
updated_at logic and existing error handling).
In `@middleware.ts`:
- Around line 125-142: The matcher in the exported config currently excludes
routes starting with "api/protected" which prevents middleware from running on
protected endpoints; update the config.matcher to explicitly include the
protected routes (e.g., add a pattern like '/api/protected/:path*' or
'/api/protected(.*)') and then use a second pattern to exclude other api routes
(the existing negative lookahead for
'/((?!api|_next/static|_next/image|favicon.ico|public).*)' or similar), so that
protectedRoutes and the middleware guard run for /api/protected while other
/api/* routes remain excluded; modify the config.matcher array in middleware.ts
to list the explicit protected route pattern first and the general exclusion
pattern second, referencing the exported symbol config and its matcher field.
- Around line 31-80: The middleware's createMiddlewareClient uses a cookies
object with no-op set/remove handlers so Supabase can't persist refreshed
tokens; update createMiddlewareClient to accept the NextResponse (created in
middleware) and implement the cookies API expected by Supabase by providing a
setAll (or set) and remove implementation that writes cookies to both the
incoming request cookies store (so downstream middleware sees updated values)
and to response.cookies (so the browser receives them). Concretely, modify
createMiddlewareClient and its cookies handlers (referencing
createMiddlewareClient, middleware, and the cookies.get/set/remove or setAll
handlers) so setAll iterates incoming cookie key/values and updates
request.cookies and response.cookies, and remove deletes from both; ensure
types/signatures match Supabase's server client expectations.
In `@schema_support_system.sql`:
- Around line 1-10: The repository has conflicting schema definitions causing
seed failures; resolve by choosing the authoritative schema and reconciling
differences: align UUID generation (replace gen_random_uuid() or
uuid_generate_v4() so both use the same function), standardize user columns (use
either first_name/last_name or full_name across tables and update CREATE TABLE
statements referencing those columns), unify RLS principal retrieval (either use
current_setting('app.current_user_id') or auth.uid() everywhere, and update
policies referencing it), and decide whether the products table belongs in the
canonical schema (either remove products from the non-authoritative file or
merge its CREATE TABLE into the main schema and update seed.sql accordingly);
update the schema file(s) and seed.sql to use the chosen symbols (uuid
generation function, full_name vs first_name/last_name, RLS function, and
products table) so definitions are compatible.
- Line 124: The metadata column definition in the refunds table has a malformed
default ('{}()') causing SQL syntax error; update the column declaration for
metadata (JSONB) to use a valid JSONB default such as an empty object cast to
JSONB (e.g., use '{}'::jsonb) or the postgres JSONB literal syntax so the
refunds table CREATE statement succeeds and the metadata column has a proper
default value.
- Around line 303-311: The current RLS policies (organizations_select_policy,
organizations_insert_policy, organizations_update_policy) use USING (true) and
WITH CHECK (true) which is too permissive; change them to enforce tenant
membership by checking that the current user is a member/owner of the
organization (e.g. replace USING (true) with a condition like EXISTS (SELECT 1
FROM organization_members om WHERE om.organization_id = organizations.id AND
om.user_id = auth.uid()) and for INSERT/UPDATE use WITH CHECK to ensure new rows
reference an organization the user is a member/owner of (e.g. WITH CHECK (EXISTS
(SELECT 1 FROM organization_members om WHERE om.organization_id =
organizations.id OR organizations.id IS NOT NULL AND om.organization_id =
NEW.organization_id AND om.user_id = auth.uid()) or check organizations.owner_id
= auth.uid() as appropriate)); update organizations_select_policy,
organizations_insert_policy and organizations_update_policy accordingly so
selects/creates/updates are limited to organization members or owners.
- Around line 499-506: The generate_ticket_number function currently uses
COUNT(*) + 1 which is not atomic; replace that logic with an atomic counter
(e.g., create a ticket_counters table with columns organization_id PK and
counter BIGINT, then inside generate_ticket_number perform an INSERT ... ON
CONFLICT (organization_id) DO UPDATE SET counter = ticket_counters.counter + 1
RETURNING counter or an UPDATE ... RETURNING to fetch the next sequence value
for that organization) and use that returned counter in the ticket string
generation instead of the COUNT expression; update references to organization_id
and tickets accordingly so ticket numbers are generated from the atomic counter
and cannot collide under concurrent transactions.
In `@supabase/schema.sql`:
- Around line 4-5: Add the missing pgvector extension so VECTOR columns and
ivfflat indexes can be created: insert a line creating the extension (e.g.
CREATE EXTENSION IF NOT EXISTS "pgvector";) alongside the existing CREATE
EXTENSION IF NOT EXISTS "uuid-ossp"; statement so the schema supports
VECTOR(1536) and the ivfflat index creation.
- Around line 517-526: The current generate_ticket_number() uses RANDOM()
causing high collision risk; replace the random suffix with a dedicated
sequence: create a sequence (e.g., ticket_number_seq) and change
generate_ticket_number() to use nextval('ticket_number_seq') (formatted/padded
with LPAD to a fixed width) when composing NEW.ticket_number (e.g., 'TKT-' ||
TO_CHAR(NOW(),'YYYYMMDD') || '-' || LPAD(nextval('ticket_number_seq')::TEXT, N,
'0')); ensure the sequence is created beforehand and the function references
nextval('ticket_number_seq') so ticket numbers are monotonic and unique under
load.
In `@supabase/seed.sql`:
- Around line 572-582: The inserted row uses an invalid enum value 'supervisor'
for the author_type column which must match the message_author_type enum
('customer','agent','system','ai'); update the tuple (the row with id
't0000004-0000-0000-0000-000000000004') to replace 'supervisor' with the correct
enum value (likely 'agent' if it's an internal human reviewer) so the INSERT
will succeed and conform to message_author_type.
🟡 Minor comments (10)
docs/PLAN.md-197-231 (1)
197-231:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks.
Markdownlint MD040 flags the fences at Line 197 and Line 357. Use
textfor the directory trees.✏️ Suggested fix
-``` +```text @@ -``` +```textAlso applies to: 357-393
docs/ARCHITECTURE_E2E.md-13-101 (1)
13-101:⚠️ Potential issue | 🟡 MinorSpecify language for fenced code blocks.
Markdownlint MD040 flags the fences at Line 13 and Line 676. Use
textfor the diagram and route tree.✏️ Suggested fix
-``` +```text @@ -``` +```textAlso applies to: 676-705
docs/ARCHITECTURE_E2E.md-412-431 (1)
412-431:⚠️ Potential issue | 🟡 MinorFix malformed cookie block in the auth helper snippet.
The code sample has a broken
cookiesblock (Line 419), which makes the snippet invalid to copy/paste.✅ Corrected snippet
- { - get(name cookies: { -: string) { - return cookieStore.get(name)?.value; - }, - set(name: string, value: string, options: any) { - cookieStore.set({ name, value, ...options }); - }, - remove(name: string, options: any) { - cookieStore.delete({ name, ...options }); - }, - }, - } + { + cookies: { + get(name: string) { + return cookieStore.get(name)?.value; + }, + set(name: string, value: string, options: any) { + cookieStore.set({ name, value, ...options }); + }, + remove(name: string, options: any) { + cookieStore.delete({ name, ...options }); + }, + }, + }lib/supabase/client.ts-470-478 (1)
470-478:⚠️ Potential issue | 🟡 MinorPotential runtime error accessing
payload.old.idon DELETE events.For
postgres_changesDELETE events,payload.oldmay be incomplete or missing fields depending on Supabase replica identity settings. Ifpayload.oldis an empty object, accessing.idwill returnundefined, which is then passed to the callback.🛡️ Proposed defensive fix
} else if (payload.eventType === 'DELETE' && callbacks.onDelete) { - callbacks.onDelete(payload.old.id); + const deletedId = payload.old?.id; + if (deletedId) { + callbacks.onDelete(deletedId); + } }lib/chat/chat-service.ts-270-302 (1)
270-302:⚠️ Potential issue | 🟡 MinorPresence channel naming may not support shared presence visibility.
The channel name
presence:${ticketId}:${userId}includes theuserId, meaning each agent gets their own unique channel. This prevents agents from seeing each other's presence on the same ticket.If the intent is for agents to see who else is viewing a ticket, the channel should be shared:
🐛 Proposed fix for shared presence
export function trackAgentPresence( ticketId: string, userId: string, userInfo: { email: string; avatar_url?: string } ): () => void { const client = getSupabaseClient(); - const channel = client.channel(`presence:${ticketId}:${userId}`, { + const channel = client.channel(`presence:ticket:${ticketId}`, { config: { presence: { key: userId }, }, });tests/unit/supabase-sdk.test.ts-40-50 (1)
40-50:⚠️ Potential issue | 🟡 MinorAssertion
expect(error || data).toBeDefined()will always pass.The pattern
expect(error || data).toBeDefined()is a tautology — at least one oferrorordatais always defined by Supabase's response shape, so this assertion provides no value. If the intent is to verify the query executed without throwing, consider:
- Asserting
expect(error).toBeNull()when success is expected- Using
expect(data).toBeDefined()or checking array length- Or using
.not.toThrow()on the async callThis pattern appears multiple times (lines 49, 60, 71, 187, 198).
lib/chat/chat-service.ts-202-255 (1)
202-255:⚠️ Potential issue | 🟡 MinorExtract duplicate ticket number generation into a utility function.
Both
createTicketimplementations generate ticket numbers using identical logic:`TKT-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${Math.random().toString(36).substring(2, 6).toUpperCase()}`While the overall functions differ in database access patterns (Supabase client vs db object), parameter signatures, and error handling, this ticket number generation can be extracted into a shared utility to reduce duplication and ensure consistency.
lib/mcp/supabase-adapter.ts-102-123 (1)
102-123:⚠️ Potential issue | 🟡 MinorHonor
where/orderByparams infindManywrappers.
These signatures accept filters/sorts but the queries ignore them, which can surprise callers and return the wrong ordering/filtering. Either apply the parameters or remove them from the contract.🛠️ Suggested update
@@ async findMany({ where, take = 10, skip = 0, orderBy }) { let query = supabase.from('orders').select('*'); @@ - query = query - .range(skip, skip + take - 1) - .order('created_at', { ascending: false }); + query = query.range(skip, skip + take - 1); + if (orderBy && Object.keys(orderBy).length) { + for (const [column, direction] of Object.entries(orderBy)) { + query = query.order(column, { ascending: direction === 'asc' }); + } + } else { + query = query.order('created_at', { ascending: false }); + } @@ async findMany({ where, take = 10, category }) { let query = supabase.from('products').select('*'); + + if (where) { + for (const [key, value] of Object.entries(where)) { + query = query.eq(key, value); + } + } @@ async findMany({ where, orderBy }) { let query = supabase .from('messages') .select('*') .eq('ticket_id', where.ticket_id); - query = query.order('created_at', { ascending: true }); + if (orderBy && Object.keys(orderBy).length) { + for (const [column, direction] of Object.entries(orderBy)) { + query = query.order(column, { ascending: direction === 'asc' }); + } + } else { + query = query.order('created_at', { ascending: true }); + }Also applies to: 167-179, 302-313
lib/agents/supabase-tools.ts-181-203 (1)
181-203:⚠️ Potential issue | 🟡 MinorAvoid defaulting unknown products to in‑stock.
Assuming availability for missing products can lead to false promises. A safer fallback is to mark them unavailable (or return an explicit “unknown” state).🔧 Safer fallback
} else { // Product not found - could be external product results.push({ productId, productName: `Product ${productId}`, - available: true, - quantity: 100, // Assume in stock + available: false, + quantity: 0, location, }); }tests/unit/supabase.test.ts-93-114 (1)
93-114:⚠️ Potential issue | 🟡 MinorTest name/expectation mismatch: rename to reflect FK validation intent.
The test asserts failure (status >= 400) but is named "should create a ticket", suggesting success. The placeholder UUID
'00000000-0000-0000-0000-000000000001'is syntactically valid and will reliably fail due to the foreign key constraint (organization_id REFERENCES organizations(id)), not UUID format validation. Rename the test to clarify this is testing FK constraint enforcement—e.g., "should reject ticket with non-existent organization"—or seed a valid organization ID and expect 201.
🧹 Nitpick comments (12)
lib/supabase/create-client.ts (2)
16-28: Consider singleton pattern for Supabase client.Creating a new
SupabaseClienton everycreateClient()call is inefficient and can cause session synchronization issues. The Supabase documentation recommends using a singleton pattern for browser clients.♻️ Proposed refactor to singleton pattern
+let browserClient: SupabaseClient | null = null; + /** * Create a Supabase client for browser/client components * This is the main client instance used throughout the app */ export function createClient(): SupabaseClient { + if (browserClient) { + return browserClient; + } + - return createSupabaseClient( + browserClient = createSupabaseClient( env.NEXT_PUBLIC_SUPABASE_URL, env.NEXT_PUBLIC_SUPABASE_ANON_KEY, { auth: { autoRefreshToken: true, persistSession: true, detectSessionInUrl: true, }, } ); + + return browserClient; }
79-87: Add guard for SSR safety onwindowaccess.
signInWithOAuthandresetPasswordaccesswindow.location.origin, which will throw during SSR if accidentally imported in a server context. Consider adding a runtime guard.♻️ Proposed defensive check
export async function signInWithOAuth(provider: 'google' | 'github') { + if (typeof window === 'undefined') { + throw new Error('signInWithOAuth can only be called in browser context'); + } const client = createClient(); return client.auth.signInWithOAuth({ provider, options: { redirectTo: `${window.location.origin}/auth/callback`, }, }); }app/auth/signup/page.tsx (1)
4-15: UnuseduseRouterimport and variable.The
routervariable fromuseRouter()is declared but never used since the signup flow displays a success message instead of navigating. Remove the unused import and variable.♻️ Proposed cleanup
import { createClient } from '@/lib/supabase/create-client'; -import { useRouter } from 'next/navigation'; import { useState, type FormEvent } from 'react'; import { UserPlus, Mail, Lock, Loader2, User } from 'lucide-react'; export default function SignupPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [fullName, setFullName] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null); - const router = useRouter();app/auth/forgot-password/page.tsx (1)
127-130: Inconsistent navigation: useLinkinstead of<a>tag.The signup link uses a plain
<a>tag while the "Back to login" navigation on Line 42-48 uses Next.jsLinkcomponent. For consistent client-side navigation behavior, useLinkfor internal routes.♻️ Proposed fix
<p className="mt-6 text-center text-sm text-gray-600 dark:text-gray-400"> Do not have an account?{' '} - <a + <Link href="/auth/signup" className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-semibold transition-colors" > Create account - </a> + </Link> </p>components/logout-button.tsx (2)
96-140: LogoutIconButton duplicates logout logic and uses wrong spinner icon.Two issues:
The
handleLogoutlogic is duplicated betweenLogoutButtonandLogoutIconButton. Consider extracting a shared hook (e.g.,useLogout) to reduce duplication.Line 134 uses
LogOutwithanimate-spinfor the loading state, butLogOut(a door/arrow icon) doesn't visually communicate "loading" when spinning.LogoutButtoncorrectly usesLoader2for this purpose.♻️ Proposed fix for the spinner icon
+import { LogOut, Loader2 } from 'lucide-react'; ... > {loading ? ( - <LogOut className="w-5 h-5 animate-spin" /> + <Loader2 className="w-5 h-5 animate-spin" /> ) : ( <LogOut className="w-5 h-5" /> )} </button>
54-60: Consider callback timing:onLogoutSuccessfires before navigation completes.The
onLogoutSuccesscallback is invoked beforerouter.pushandrouter.refresh. If a consumer expects this callback to fire after the user is fully redirected, this order may cause confusion. Consider documenting this behavior or renaming toonLogoutStart/onBeforeRedirectif intentional.lib/supabase/client.ts (1)
15-43: Singleton pattern may cause issues in SSR/edge environments.The module-level
supabaseClientsingleton works for client-side code but can cause issues in server-side rendering or edge functions where module state may be shared across requests or reset unexpectedly. Consider documenting that this client is intended for browser use only, or use a different pattern for server contexts (the codebase appears to havelib/supabase/server.tsfor that purpose).app/dashboard/layout.tsx (1)
22-22: Unused import:useAuthis imported but not used.The
useAuthhook is imported but doesn't appear to be used anywhere in this file. Either remove the unused import or utilize it (e.g., to display the actual logged-in user's name instead of the hardcoded "Agent Smith" on line 166).♻️ Option 1: Remove unused import
import { LogoutButton } from '@/components/logout-button'; -import { useAuth } from '@/components/auth-provider';♻️ Option 2: Use auth context for user info
export default function DashboardLayout({ children }: DashboardLayoutProps) { const pathname = usePathname(); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [notifications] = useState(3); + const { user } = useAuth(); ... - <p className="text-sm font-medium text-gray-900 dark:text-white">Agent Smith</p> - <p className="text-xs text-gray-500 dark:text-gray-400">Support Lead</p> + <p className="text-sm font-medium text-gray-900 dark:text-white">{user?.email ?? 'Agent'}</p> + <p className="text-xs text-gray-500 dark:text-gray-400">{user?.user_metadata?.role ?? 'Support'}</p>tests/unit/supabase-sdk.test.ts (2)
330-369: "Chat Service Integration" tests don't actually test thechat-service.tsmodule.These tests define local mock objects and mapping logic instead of importing and testing the actual
toChatMessagefunction fromlib/chat/chat-service.ts. This means any bugs in the actual module won't be caught.Consider importing the actual functions:
♻️ Proposed fix to test actual module
+import { toChatMessage, type ChatMessage } from '@/lib/chat/chat-service'; +import type { Message } from '@/lib/supabase/client'; + describe('Chat Service Integration', () => { - beforeAll(() => { - supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); - }); - it('should format Supabase message to chat message', () => { - // Test message formatting logic - const mockSupabaseMessage = { + const mockSupabaseMessage: Message = { id: 'test-msg-1', ticket_id: 'test-ticket-1', author_id: 'test-user-1', author_type: 'customer', + author_name: null, content: 'Hello, I need help', content_type: 'text', attachments: [], is_internal: false, + read_at: null, + metadata: null, created_at: new Date().toISOString(), }; - // Verify structure matches expected format - expect(mockSupabaseMessage).toHaveProperty('id'); - ... + const chatMessage = toChatMessage(mockSupabaseMessage); + expect(chatMessage.id).toBe('test-msg-1'); + expect(chatMessage.role).toBe('user'); + expect(chatMessage.content).toBe('Hello, I need help'); + expect(chatMessage.status).toBe('complete'); });Based on learnings: "Create test suite files for verifying implementation correctness" — the tests should exercise the actual implementation.
34-36: Unused variabletestUserIddeclared but never assigned or used.
testUserIdis declared on line 35 but is never populated or referenced in any test. Either remove it or implement tests that use it.♻️ Proposed fix
describe('Schema Tables CRUD (SDK)', () => { let testOrgId: string; - let testUserId: string; let testTicketId: string; let testMessageId: string;lib/mcp/supabase-adapter.ts (1)
437-441: Make unsupported cart operations explicit.
Returningnullcan be misinterpreted as a successful read/write and mask missing functionality. Consider throwing a clear error instead.💡 Fail-fast suggestion
cart: { - findUnique: async () => null, // Not implemented for Supabase - create: async () => null, - update: async () => null, + findUnique: async () => { + throw new Error('Cart operations are not supported in Supabase'); + }, + create: async () => { + throw new Error('Cart operations are not supported in Supabase'); + }, + update: async () => { + throw new Error('Cart operations are not supported in Supabase'); + }, },lib/agents/supabase-tools.ts (1)
20-26:minScoreis defined but unused and not exposed to tool callers.
The schema includesminScore,productSearchignores it, and the tool definitions omit it. Either wire it into filtering when similarity scores are available or remove it for now.♻️ Simplify until scoring is implemented
export const ProductSearchInputSchema = z.object({ query: z.string().min(1).describe('Natural language search query'), limit: z.number().int().positive().default(10).describe('Maximum results to return'), - minScore: z.number().min(0).max(1).default(0.5).describe('Minimum similarity score'), category: z.string().optional().describe('Filter by product category'), priceRange: z.array(z.number()).length(2).optional().describe('Price filter [min, max]'), }); @@ - const { query, limit = 10, minScore = 0.5, category, priceRange } = input; + const { query, limit = 10, category, priceRange } = input;Also applies to: 121-144, 401-415
| const { searchParams, origin } = new URL(request.url); | ||
| const code = searchParams.get('code'); | ||
| const next = searchParams.get('next') ?? '/dashboard'; | ||
|
|
||
| // If there's no code, redirect to login with error | ||
| if (!code) { | ||
| return NextResponse.redirect(`${origin}/auth/login?error=no_code`); | ||
| } | ||
|
|
||
| // Create server client with cookie handling | ||
| const cookieStore = await cookies(); | ||
| const supabase = createServerClient( | ||
| process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||
| process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, | ||
| { | ||
| cookies: { | ||
| get(name: string) { | ||
| return cookieStore.get(name)?.value; | ||
| }, | ||
| set(name: string, value: string, options: { path?: string; domain?: string; sameSite?: 'lax' | 'strict' | 'none'; secure?: boolean; httpOnly?: boolean; maxAge?: number }) { | ||
| try { | ||
| cookieStore.set({ name, value, ...options }); | ||
| } catch { | ||
| // Ignore errors during callback | ||
| } | ||
| }, | ||
| remove(name: string, options: { path?: string; domain?: string }) { | ||
| try { | ||
| cookieStore.set({ name, value: '', ...options, maxAge: 0 }); | ||
| } catch { | ||
| // Ignore errors during callback | ||
| } | ||
| }, | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| // Exchange the code for a session | ||
| const { error } = await supabase.auth.exchangeCodeForSession(code); | ||
|
|
||
| if (error) { | ||
| console.error('[Auth Callback] Session exchange error:', error.message); | ||
| return NextResponse.redirect(`${origin}/auth/login?error=${encodeURIComponent(error.message)}`); | ||
| } | ||
|
|
||
| // Successful authentication - redirect to dashboard or specified next page | ||
| return NextResponse.redirect(`${origin}${next}`); |
There was a problem hiding this comment.
Validate next to prevent open redirects.
next is user-controlled (Line 19) and concatenated into the redirect URL (Line 63). Values like @evil.com can yield a redirect of the form https://your.app@evil.com. Restrict to safe relative paths and construct the redirect with new URL(...).
🔒 Suggested hardening
- const next = searchParams.get('next') ?? '/dashboard';
+ const nextParam = searchParams.get('next');
+ const next =
+ nextParam &&
+ nextParam.startsWith('/') &&
+ !nextParam.startsWith('//') &&
+ !nextParam.includes('@')
+ ? nextParam
+ : '/dashboard';
...
- return NextResponse.redirect(`${origin}${next}`);
+ return NextResponse.redirect(new URL(next, origin));🤖 Prompt for AI Agents
In `@app/auth/callback/route.ts` around lines 17 - 63, The redirect uses the
user-controlled variable next and directly concatenates it into
NextResponse.redirect, allowing open-redirects; validate and normalize next
before redirecting by ensuring it is a safe relative path (e.g., non-empty,
starts with a single '/' but not '//' and contains no scheme or '@'), fallback
to '/dashboard' if invalid, and build the final redirect using new
URL(validatedNext, origin) (use origin and
NextResponse.redirect(newUrl.toString())) so the target is guaranteed
same-origin; reference the next variable, origin, request.url, and the
NextResponse.redirect call in your change.
| /** | ||
| * Sign out the current user | ||
| */ | ||
| const signOut = async () => { | ||
| const supabase = createClient(); | ||
| await supabase.auth.signOut(); | ||
| setSession(null); | ||
| setUser(null); | ||
| }; | ||
|
|
||
| /** | ||
| * Force refresh the session | ||
| */ | ||
| const refreshSession = async () => { | ||
| const supabase = createClient(); | ||
| const { data: { session } } = await supabase.auth.getSession(); | ||
| setSession(session); | ||
| setUser(session?.user ?? null); | ||
| }; |
There was a problem hiding this comment.
signOut and refreshSession create new client instances, causing inconsistency.
The useEffect creates a Supabase client and subscribes to auth state changes on that instance. However, signOut and refreshSession each create new client instances via createClient(). This means:
- Auth state changes triggered by these new clients won't be observed by the original subscription (though Supabase may still sync via storage).
refreshSessioncallsgetSession()instead ofrefreshSession(), which doesn't actually refresh the token — it just reads the current session.
Consider storing the client in a ref or using the same instance throughout:
🐛 Proposed fix
+import { useRef } from 'react';
+
export function AuthProvider({ children }: { children: ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
+ const supabaseRef = useRef(createClient());
useEffect(() => {
- const supabase = createClient();
+ const supabase = supabaseRef.current;
// ... rest of effect
}, []);
const signOut = async () => {
- const supabase = createClient();
+ const supabase = supabaseRef.current;
await supabase.auth.signOut();
setSession(null);
setUser(null);
};
const refreshSession = async () => {
- const supabase = createClient();
- const { data: { session } } = await supabase.auth.getSession();
+ const supabase = supabaseRef.current;
+ const { data: { session } } = await supabase.auth.refreshSession();
setSession(session);
setUser(session?.user ?? null);
};🤖 Prompt for AI Agents
In `@components/auth-provider.tsx` around lines 68 - 86, The signOut and
refreshSession functions create new Supabase clients causing subscription
mismatch and refreshSession only reads the session instead of refreshing it; fix
by reusing the same client instance created in the useEffect (e.g., store it in
a ref like supabaseRef.current when you call createClient() in useEffect) and
update signOut and refreshSession to use that ref instead of calling
createClient(); also change refreshSession to call
supabase.auth.refreshSession() (and then setSession/setUser from the returned
session) rather than auth.getSession().
| try { | ||
| const db = getDb(); | ||
| const orders = await db.orders.findMany({ | ||
| where: input.email ? { customer_email: input.email } : {}, | ||
| take: input.limit, | ||
| }); |
There was a problem hiding this comment.
orderId/status inputs are ignored in lookup.
orderLookup currently filters only by email, so callers providing orderId or status get incorrect results (potentially all orders). Build the where filter from the provided inputs.
🧭 Suggested filter handling
- const orders = await db.orders.findMany({
- where: input.email ? { customer_email: input.email } : {},
- take: input.limit,
- });
+ const where: Record<string, unknown> = {};
+ if (input.orderId) where.id = input.orderId;
+ if (input.email) where.customer_email = input.email;
+ if (input.status) where.status = input.status;
+
+ const orders = await db.orders.findMany({
+ where: Object.keys(where).length ? where : undefined,
+ take: input.limit,
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const db = getDb(); | |
| const orders = await db.orders.findMany({ | |
| where: input.email ? { customer_email: input.email } : {}, | |
| take: input.limit, | |
| }); | |
| try { | |
| const db = getDb(); | |
| const where: Record<string, unknown> = {}; | |
| if (input.orderId) where.id = input.orderId; | |
| if (input.email) where.customer_email = input.email; | |
| if (input.status) where.status = input.status; | |
| const orders = await db.orders.findMany({ | |
| where: Object.keys(where).length ? where : undefined, | |
| take: input.limit, | |
| }); |
🤖 Prompt for AI Agents
In `@lib/agents/supabase-tools.ts` around lines 233 - 238, The order lookup
currently only filters by email; update the where clause used in
getDb().orders.findMany inside the orderLookup logic to build a dynamic filter
object that includes customer_email when input.email is present, id/order_id
when input.orderId is present, and status when input.status is present (only add
each key if its corresponding input is provided), then pass that composed where
object to db.orders.findMany along with take: input.limit; use getDb() and
db.orders.findMany as the insertion points to locate and replace the existing
hardcoded where: input.email ? { customer_email: input.email } : {} logic.
| try { | ||
| const db = getDb(); | ||
|
|
||
| // Generate refund number | ||
| const refundNumber = `REF-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${Math.random().toString(36).substring(2, 6).toUpperCase()}`; | ||
|
|
||
| // Create refund record | ||
| const refund = await db.refunds.create({ | ||
| data: { | ||
| order_id: input.orderId, | ||
| amount: input.amount, | ||
| reason: input.reason, | ||
| idempotency_key: input.idempotencyKey, | ||
| status: 'pending', | ||
| customer_email: '', // Would need to fetch from order | ||
| }, | ||
| }) as any; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find database schema files and migrations
fd -e sql -e ts -e js | grep -E "(migration|schema|database)" | head -20Repository: Aparnap2/smart_commerce_agent
Length of output: 457
🏁 Script executed:
# Search for refunds table definition
rg "refunds" -t sql -t ts -B 3 -A 10 | head -100Repository: Aparnap2/smart_commerce_agent
Length of output: 6551
🏁 Script executed:
# Look for idempotency_key mentions in the codebase
rg "idempotency_key" -B 2 -A 2Repository: Aparnap2/smart_commerce_agent
Length of output: 3003
🏁 Script executed:
# Get the full context of the refund creation function
cat lib/agents/supabase-tools.ts | head -400 | tail -200Repository: Aparnap2/smart_commerce_agent
Length of output: 5848
🏁 Script executed:
# Verify idempotency_key field in full refunds table schema
cat schema_support_system.sql | grep -A 30 "CREATE TABLE IF NOT EXISTS refunds"Repository: Aparnap2/smart_commerce_agent
Length of output: 1624
🏁 Script executed:
# Check Prisma refund model for constraints
rg -A 20 "model Refund" prisma/schema.prismaRepository: Aparnap2/smart_commerce_agent
Length of output: 845
🏁 Script executed:
# Search for any migration or schema that defines idempotency_key in refunds table
rg "idempotency_key" -t sqlRepository: Aparnap2/smart_commerce_agent
Length of output: 55
🏁 Script executed:
# Check if there are multiple schema definitions
fd -name "*.sql" -type f | xargs grep -l "CREATE TABLE.*refunds"Repository: Aparnap2/smart_commerce_agent
Length of output: 305
🏁 Script executed:
# Verify complete Prisma refund model
cat prisma/schema.prisma | grep -B 5 -A 20 "model Refund"Repository: Aparnap2/smart_commerce_agent
Length of output: 995
🏁 Script executed:
# Check all migration files for idempotency_key definition
find prisma/migrations -name "*.sql" -type f -exec grep -l "idempotency_key" {} \;Repository: Aparnap2/smart_commerce_agent
Length of output: 55
🏁 Script executed:
# List all migration files to understand order
ls -la prisma/migrations/Repository: Aparnap2/smart_commerce_agent
Length of output: 528
Idempotency not enforced; retries can create duplicate refunds.
The idempotencyKey is accepted but the code always inserts a new refund without checking if one with the same key already exists. No database-level unique constraint exists on idempotency_key (Prisma model lacks @unique, SQL schema has no constraint, and no migration defines it). If the tool is retried, duplicate refunds will be created.
Implement one of the following:
- Query for an existing refund by
idempotencyKeybefore insert and return it if found, or - Add a unique constraint on
idempotency_keyin the database schema and migrations.
🛡️ Idempotency guard
try {
const db = getDb();
+ const { data: existing, error: existingError } = await db.client
+ .from('refunds')
+ .select('*')
+ .eq('idempotency_key', input.idempotencyKey)
+ .maybeSingle();
+ if (existingError) {
+ throw new Error(`Failed to check idempotency: ${existingError.message}`);
+ }
+ if (existing) {
+ return {
+ success: true,
+ result: {
+ success: true,
+ refundId: existing.id,
+ status: (existing.status ?? 'pending') as RefundResult['status'],
+ amount: existing.amount ?? input.amount,
+ message: 'Refund request already exists',
+ },
+ };
+ }
+
// Generate refund number
const refundNumber = `REF-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${Math.random().toString(36).substring(2, 6).toUpperCase()}`;🤖 Prompt for AI Agents
In `@lib/agents/supabase-tools.ts` around lines 275 - 291, The code always inserts
a new refund via getDb() -> db.refunds.create and ignores input.idempotencyKey,
so retries can create duplicates; fix by adding an idempotency guard: before
calling db.refunds.create check for an existing record using the idempotency key
(e.g., query db.refunds.findFirst or equivalent filtering on idempotency_key ===
input.idempotencyKey and optionally order_id === input.orderId) and return that
refund if found; alternatively, enforce uniqueness at the DB/schema level by
marking idempotency_key as unique in your ORM/schema (e.g., add `@unique` to
idempotency_key and create a migration) and update the create flow to handle
unique-constraint violations (catch the error, re-query by idempotency_key and
return the existing refund) while keeping refundNumber and other logic intact.
| export async function update<T>( | ||
| table: string, | ||
| id: string, | ||
| updates: Partial<T> | ||
| ): Promise<T | null> { | ||
| const client = getSupabaseClient(); | ||
| const { data, error } = await client | ||
| .from(table) | ||
| .update({ ...updates, updated_at: new Date().toISOString() } as Record<string, unknown>) | ||
| .eq('id', id) | ||
| .single(); | ||
|
|
||
| if (error) { | ||
| console.error(`[Supabase] Update error on ${table}:`, error); | ||
| throw new Error(`Failed to update ${table}: ${error.message}`); | ||
| } | ||
|
|
||
| return data as T | null; | ||
| } |
There was a problem hiding this comment.
update function missing .select() — will return null instead of updated record.
The update function calls .single() without .select(), which means Supabase won't return the updated data. According to Supabase JS behavior, you must call .select() before .single() to receive the updated record. Currently, this will always return null for data.
🐛 Proposed fix
export async function update<T>(
table: string,
id: string,
updates: Partial<T>
): Promise<T | null> {
const client = getSupabaseClient();
const { data, error } = await client
.from(table)
.update({ ...updates, updated_at: new Date().toISOString() } as Record<string, unknown>)
.eq('id', id)
+ .select()
.single();
if (error) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function update<T>( | |
| table: string, | |
| id: string, | |
| updates: Partial<T> | |
| ): Promise<T | null> { | |
| const client = getSupabaseClient(); | |
| const { data, error } = await client | |
| .from(table) | |
| .update({ ...updates, updated_at: new Date().toISOString() } as Record<string, unknown>) | |
| .eq('id', id) | |
| .single(); | |
| if (error) { | |
| console.error(`[Supabase] Update error on ${table}:`, error); | |
| throw new Error(`Failed to update ${table}: ${error.message}`); | |
| } | |
| return data as T | null; | |
| } | |
| export async function update<T>( | |
| table: string, | |
| id: string, | |
| updates: Partial<T> | |
| ): Promise<T | null> { | |
| const client = getSupabaseClient(); | |
| const { data, error } = await client | |
| .from(table) | |
| .update({ ...updates, updated_at: new Date().toISOString() } as Record<string, unknown>) | |
| .eq('id', id) | |
| .select() | |
| .single(); | |
| if (error) { | |
| console.error(`[Supabase] Update error on ${table}:`, error); | |
| throw new Error(`Failed to update ${table}: ${error.message}`); | |
| } | |
| return data as T | null; | |
| } |
🤖 Prompt for AI Agents
In `@lib/supabase/client.ts` around lines 303 - 321, The update function (exported
as update) is calling .single() without .select(), so Supabase returns null;
update the call chain on the Supabase client (the expression starting from
client.from(table).update(...)) to request the updated row by adding
.select('*') (or the explicit column list) before .single(), ensuring the
function returns the updated record; no other behavior changes needed (keep
updated_at logic and existing error handling).
| -- Organizations: Users can only see their own organization | ||
| CREATE POLICY "organizations_select_policy" ON organizations | ||
| FOR SELECT USING (true); | ||
|
|
||
| CREATE POLICY "organizations_insert_policy" ON organizations | ||
| FOR INSERT WITH CHECK (true); | ||
|
|
||
| CREATE POLICY "organizations_update_policy" ON organizations | ||
| FOR UPDATE USING (true); |
There was a problem hiding this comment.
Overly permissive RLS policies on organizations table.
The organizations table policies use USING (true) which allows any user to SELECT, INSERT, and UPDATE any organization. This breaks tenant isolation and is a security vulnerability.
🔒 Suggested fix - restrict to organization members
CREATE POLICY "organizations_select_policy" ON organizations
- FOR SELECT USING (true);
+ FOR SELECT USING (
+ id IN (SELECT organization_id FROM users WHERE id = current_setting('app.current_user_id', true)::UUID)
+ );
CREATE POLICY "organizations_insert_policy" ON organizations
- FOR INSERT WITH CHECK (true);
+ FOR INSERT WITH CHECK (false); -- Only allow through admin API
CREATE POLICY "organizations_update_policy" ON organizations
- FOR UPDATE USING (true);
+ FOR UPDATE USING (
+ id IN (SELECT organization_id FROM users WHERE id = current_setting('app.current_user_id', true)::UUID AND role IN ('owner', 'admin'))
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| -- Organizations: Users can only see their own organization | |
| CREATE POLICY "organizations_select_policy" ON organizations | |
| FOR SELECT USING (true); | |
| CREATE POLICY "organizations_insert_policy" ON organizations | |
| FOR INSERT WITH CHECK (true); | |
| CREATE POLICY "organizations_update_policy" ON organizations | |
| FOR UPDATE USING (true); | |
| -- Organizations: Users can only see their own organization | |
| CREATE POLICY "organizations_select_policy" ON organizations | |
| FOR SELECT USING ( | |
| id IN (SELECT organization_id FROM users WHERE id = current_setting('app.current_user_id', true)::UUID) | |
| ); | |
| CREATE POLICY "organizations_insert_policy" ON organizations | |
| FOR INSERT WITH CHECK (false); -- Only allow through admin API | |
| CREATE POLICY "organizations_update_policy" ON organizations | |
| FOR UPDATE USING ( | |
| id IN (SELECT organization_id FROM users WHERE id = current_setting('app.current_user_id', true)::UUID AND role IN ('owner', 'admin')) | |
| ); |
🤖 Prompt for AI Agents
In `@schema_support_system.sql` around lines 303 - 311, The current RLS policies
(organizations_select_policy, organizations_insert_policy,
organizations_update_policy) use USING (true) and WITH CHECK (true) which is too
permissive; change them to enforce tenant membership by checking that the
current user is a member/owner of the organization (e.g. replace USING (true)
with a condition like EXISTS (SELECT 1 FROM organization_members om WHERE
om.organization_id = organizations.id AND om.user_id = auth.uid()) and for
INSERT/UPDATE use WITH CHECK to ensure new rows reference an organization the
user is a member/owner of (e.g. WITH CHECK (EXISTS (SELECT 1 FROM
organization_members om WHERE om.organization_id = organizations.id OR
organizations.id IS NOT NULL AND om.organization_id = NEW.organization_id AND
om.user_id = auth.uid()) or check organizations.owner_id = auth.uid() as
appropriate)); update organizations_select_policy, organizations_insert_policy
and organizations_update_policy accordingly so selects/creates/updates are
limited to organization members or owners.
| CREATE OR REPLACE FUNCTION generate_ticket_number(organization_id UUID) | ||
| RETURNS VARCHAR(50) AS $$ | ||
| BEGIN | ||
| RETURN 'TKT-' || organization_id::TEXT::BIGINT % 1000000 || '-' || EXTRACT(YEAR FROM NOW()) || | ||
| LPAD(EXTRACT(MONTH FROM NOW())::TEXT, 2, '0') || | ||
| LPAD((SELECT COUNT(*) + 1 FROM tickets WHERE organization_id = generate_ticket_number.organization_id)::TEXT, 6, '0'); | ||
| END; | ||
| $$ LANGUAGE plpgsql; |
There was a problem hiding this comment.
Race condition in ticket number generation.
Using COUNT(*) + 1 to generate sequential ticket numbers is not atomic. Concurrent transactions can read the same count before either commits, causing duplicate ticket numbers and unique constraint violations.
♻️ Proposed fix using sequence
+CREATE SEQUENCE IF NOT EXISTS ticket_number_seq;
+
CREATE OR REPLACE FUNCTION generate_ticket_number(organization_id UUID)
RETURNS VARCHAR(50) AS $$
BEGIN
- RETURN 'TKT-' || organization_id::TEXT::BIGINT % 1000000 || '-' || EXTRACT(YEAR FROM NOW()) ||
- LPAD(EXTRACT(MONTH FROM NOW())::TEXT, 2, '0') ||
- LPAD((SELECT COUNT(*) + 1 FROM tickets WHERE organization_id = generate_ticket_number.organization_id)::TEXT, 6, '0');
+ RETURN 'TKT-' || LPAD((organization_id::TEXT::BIGINT % 1000000)::TEXT, 6, '0') || '-' ||
+ TO_CHAR(NOW(), 'YYYYMM') || '-' ||
+ LPAD(nextval('ticket_number_seq')::TEXT, 6, '0');
END;
$$ LANGUAGE plpgsql;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE OR REPLACE FUNCTION generate_ticket_number(organization_id UUID) | |
| RETURNS VARCHAR(50) AS $$ | |
| BEGIN | |
| RETURN 'TKT-' || organization_id::TEXT::BIGINT % 1000000 || '-' || EXTRACT(YEAR FROM NOW()) || | |
| LPAD(EXTRACT(MONTH FROM NOW())::TEXT, 2, '0') || | |
| LPAD((SELECT COUNT(*) + 1 FROM tickets WHERE organization_id = generate_ticket_number.organization_id)::TEXT, 6, '0'); | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| CREATE SEQUENCE IF NOT EXISTS ticket_number_seq; | |
| CREATE OR REPLACE FUNCTION generate_ticket_number(organization_id UUID) | |
| RETURNS VARCHAR(50) AS $$ | |
| BEGIN | |
| RETURN 'TKT-' || LPAD((organization_id::TEXT::BIGINT % 1000000)::TEXT, 6, '0') || '-' || | |
| TO_CHAR(NOW(), 'YYYYMM') || '-' || | |
| LPAD(nextval('ticket_number_seq')::TEXT, 6, '0'); | |
| END; | |
| $$ LANGUAGE plpgsql; |
🤖 Prompt for AI Agents
In `@schema_support_system.sql` around lines 499 - 506, The generate_ticket_number
function currently uses COUNT(*) + 1 which is not atomic; replace that logic
with an atomic counter (e.g., create a ticket_counters table with columns
organization_id PK and counter BIGINT, then inside generate_ticket_number
perform an INSERT ... ON CONFLICT (organization_id) DO UPDATE SET counter =
ticket_counters.counter + 1 RETURNING counter or an UPDATE ... RETURNING to
fetch the next sequence value for that organization) and use that returned
counter in the ticket string generation instead of the COUNT expression; update
references to organization_id and tickets accordingly so ticket numbers are
generated from the atomic counter and cannot collide under concurrent
transactions.
| -- Enable UUID extension | ||
| CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; |
There was a problem hiding this comment.
Missing pgvector extension for VECTOR type.
The schema uses VECTOR(1536) on Line 245 and creates an ivfflat index on Lines 257-258, but the pgvector extension is not enabled. This will cause the schema to fail on deployment.
🐛 Proposed fix to add pgvector extension
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+-- Enable pgvector extension for embeddings
+CREATE EXTENSION IF NOT EXISTS "vector";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| -- Enable UUID extension | |
| CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; | |
| -- Enable UUID extension | |
| CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; | |
| -- Enable pgvector extension for embeddings | |
| CREATE EXTENSION IF NOT EXISTS "vector"; |
🤖 Prompt for AI Agents
In `@supabase/schema.sql` around lines 4 - 5, Add the missing pgvector extension
so VECTOR columns and ivfflat indexes can be created: insert a line creating the
extension (e.g. CREATE EXTENSION IF NOT EXISTS "pgvector";) alongside the
existing CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; statement so the schema
supports VECTOR(1536) and the ivfflat index creation.
| CREATE OR REPLACE FUNCTION generate_ticket_number() | ||
| RETURNS TRIGGER AS $$ | ||
| BEGIN | ||
| IF NEW.ticket_number IS NULL THEN | ||
| NEW.ticket_number := 'TKT-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' || | ||
| LPAD(FLOOR(RANDOM() * 10000)::TEXT, 4, '0'); | ||
| END IF; | ||
| RETURN NEW; | ||
| END; | ||
| $$ LANGUAGE plpgsql; |
There was a problem hiding this comment.
Ticket number generation may cause collisions under load.
The generate_ticket_number() function uses FLOOR(RANDOM() * 10000) which provides only 10,000 unique values per day. Due to the birthday paradox, collision probability reaches ~50% after approximately 118 tickets per day, causing insert failures on the UNIQUE constraint.
Consider using a sequence or a more robust approach:
♻️ Proposed fix using sequence-based generation
+-- Create sequence for ticket numbers
+CREATE SEQUENCE IF NOT EXISTS ticket_number_seq;
+
CREATE OR REPLACE FUNCTION generate_ticket_number()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.ticket_number IS NULL THEN
- NEW.ticket_number := 'TKT-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' ||
- LPAD(FLOOR(RANDOM() * 10000)::TEXT, 4, '0');
+ NEW.ticket_number := 'TKT-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' ||
+ LPAD(nextval('ticket_number_seq')::TEXT, 6, '0');
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE OR REPLACE FUNCTION generate_ticket_number() | |
| RETURNS TRIGGER AS $$ | |
| BEGIN | |
| IF NEW.ticket_number IS NULL THEN | |
| NEW.ticket_number := 'TKT-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' || | |
| LPAD(FLOOR(RANDOM() * 10000)::TEXT, 4, '0'); | |
| END IF; | |
| RETURN NEW; | |
| END; | |
| $$ LANGUAGE plpgsql; | |
| -- Create sequence for ticket numbers | |
| CREATE SEQUENCE IF NOT EXISTS ticket_number_seq; | |
| CREATE OR REPLACE FUNCTION generate_ticket_number() | |
| RETURNS TRIGGER AS $$ | |
| BEGIN | |
| IF NEW.ticket_number IS NULL THEN | |
| NEW.ticket_number := 'TKT-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' || | |
| LPAD(nextval('ticket_number_seq')::TEXT, 6, '0'); | |
| END IF; | |
| RETURN NEW; | |
| END; | |
| $$ LANGUAGE plpgsql; |
🤖 Prompt for AI Agents
In `@supabase/schema.sql` around lines 517 - 526, The current
generate_ticket_number() uses RANDOM() causing high collision risk; replace the
random suffix with a dedicated sequence: create a sequence (e.g.,
ticket_number_seq) and change generate_ticket_number() to use
nextval('ticket_number_seq') (formatted/padded with LPAD to a fixed width) when
composing NEW.ticket_number (e.g., 'TKT-' || TO_CHAR(NOW(),'YYYYMMDD') || '-' ||
LPAD(nextval('ticket_number_seq')::TEXT, N, '0')); ensure the sequence is
created beforehand and the function references nextval('ticket_number_seq') so
ticket numbers are monotonic and unique under load.
| ( | ||
| 't0000004-0000-0000-0000-000000000004', | ||
| 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', | ||
| 'u0000003-0000-0000-0000-000000000003', | ||
| 'supervisor', | ||
| '[INTERNAL] Customer is within 30-day refund window (purchased Nov 5). Enterprise policy allows full refund with documentation of decision. Need Mike to review and approve.', | ||
| '<p><strong>[INTERNAL]</strong> Customer is within 30-day refund window (purchased Nov 5). Enterprise policy allows full refund with documentation of decision. Need Mike to review and approve.</p>', | ||
| TRUE, | ||
| '[]'::jsonb, | ||
| '2024-12-03 10:30:00+00' | ||
| ), |
There was a problem hiding this comment.
Invalid enum value for author_type.
The author_type value 'supervisor' on Line 576 is not valid according to the message_author_type enum defined in supabase/schema.sql, which only allows: 'customer', 'agent', 'system', 'ai'. This will cause an insert failure.
🐛 Proposed fix
(
't0000004-0000-0000-0000-000000000004',
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'u0000003-0000-0000-0000-000000000003',
- 'supervisor',
+ 'agent',
'[INTERNAL] Customer is within 30-day refund window (purchased Nov 5). Enterprise policy allows full refund with documentation of decision. Need Mike to review and approve.',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ( | |
| 't0000004-0000-0000-0000-000000000004', | |
| 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', | |
| 'u0000003-0000-0000-0000-000000000003', | |
| 'supervisor', | |
| '[INTERNAL] Customer is within 30-day refund window (purchased Nov 5). Enterprise policy allows full refund with documentation of decision. Need Mike to review and approve.', | |
| '<p><strong>[INTERNAL]</strong> Customer is within 30-day refund window (purchased Nov 5). Enterprise policy allows full refund with documentation of decision. Need Mike to review and approve.</p>', | |
| TRUE, | |
| '[]'::jsonb, | |
| '2024-12-03 10:30:00+00' | |
| ), | |
| ( | |
| 't0000004-0000-0000-0000-000000000004', | |
| 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', | |
| 'u0000003-0000-0000-0000-000000000003', | |
| 'agent', | |
| '[INTERNAL] Customer is within 30-day refund window (purchased Nov 5). Enterprise policy allows full refund with documentation of decision. Need Mike to review and approve.', | |
| '<p><strong>[INTERNAL]</strong> Customer is within 30-day refund window (purchased Nov 5). Enterprise policy allows full refund with documentation of decision. Need Mike to review and approve.</p>', | |
| TRUE, | |
| '[]'::jsonb, | |
| '2024-12-03 10:30:00+00' | |
| ), |
🤖 Prompt for AI Agents
In `@supabase/seed.sql` around lines 572 - 582, The inserted row uses an invalid
enum value 'supervisor' for the author_type column which must match the
message_author_type enum ('customer','agent','system','ai'); update the tuple
(the row with id 't0000004-0000-0000-0000-000000000004') to replace 'supervisor'
with the correct enum value (likely 'agent' if it's an internal human reviewer)
so the INSERT will succeed and conform to message_author_type.
- Add E2E workflow tests for complete user journeys - Add Ollama LLM integration tests (qwen2.5-coder:3b, nomic-embed-text) - Add Supabase SDK tests for CRUD, Auth, and Realtime - Add RAG service and MCP tools tests - Add Node.js mock server for API mocking - Add health check script for all services - Add DeepEval configuration for LLM evaluation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/unit/rag.service.test.ts (1)
210-224:⚠️ Potential issue | 🟡 MinorMock
queryDatabaseto return rows array, not a single object.
queryDatabasereturnsrows(array) in production; this mock should mirror that to avoid false positives.🔧 Suggested fix
- (queryDatabase as vi.Mock).mockResolvedValueOnce({ id: 'doc-1' }); + (queryDatabase as vi.Mock).mockResolvedValueOnce([{ id: 'doc-1' }]);
🤖 Fix all issues with AI agents
In `@mocks/mock-server.js`:
- Around line 90-103: The async request handlers for routes '/api/auth/login',
'/api/tickets' (POST/PUT), '/api/tickets/:id/messages', and
'/api/webhooks/stripe' call JSON.parse inside the req.on('end') callback without
try/catch; wrap the JSON.parse and subsequent payload handling in a try/catch
within each req.on('end') listener (the anonymous callback passed to
req.on('end')) and on parse error reply with a 400 response (res.writeHead(400)
/ res.end with an error JSON) so the server does not crash and the endpoint
returns a proper client error.
In `@scripts/health-check.sh`:
- Around line 17-21: The health-check defaults collide because MOCKOON_URL and
NEXTJS_URL both default to port 3000; update the environment default for
MOCKOON_URL (or derive it from NEXTJS_URL) so services use distinct ports —
e.g., change the MOCKOON_URL default from "http://localhost:3000" to a different
port like "http://localhost:3001" (or programmatically set
MOCKOON_URL="${MOCKOON_URL:-${NEXTJS_URL/http:\/\/localhost:3000/http:\/\/localhost:3001}}")
so that MOCKOON_URL and NEXTJS_URL do not conflict in the health-check script.
- Around line 273-276: Replace the current export pipeline that uses
cat/grep/xargs with a safe sourcing approach: inside the existing if [ -f
.env.test.docker ]; then ... fi block, use "set -a; . .env.test.docker; set +a"
(or "set -a; source .env.test.docker; set +a") so variables from
.env.test.docker are exported without word-splitting; ensure the conditional
(the if [ -f .env.test.docker ]; then) and the file name ".env.test.docker"
remain the same.
- Around line 84-107: The curl command substitutions used to set RESPONSE and
EMBEDDING (and the third unshown curl at the other location) can cause the
script to exit immediately under set -e on network failure, so update the
assignments that call curl (the ones assigning RESPONSE and EMBEDDING and the
third curl assignment) to append || true to the command substitution to prevent
early termination and allow the subsequent error-handling (the if/else checks
and log_fail/log_info) to run; ensure you only change how the curl substitutions
are invoked (add || true) and do not alter the subsequent checks that inspect
RESPONSE or EMBEDDING.
In `@tests/e2e/workflow.test.ts`:
- Around line 11-29: The test gating uses conditionalDescribe computed at module
load while mockoonAvailable is still false and also incorrectly passes a
nonexistent timeout option to fetch; fix by performing the availability check at
module top-level before computing conditionalDescribe (use top-level await to
HEAD API_BASE), use AbortController/AbortSignal to implement a timeout for the
fetch, set mockoonAvailable based on response.ok, then compute
conditionalDescribe = mockoonAvailable ? describe : describe.skip so the suite
is gated deterministically; update references to mockoonAvailable,
conditionalDescribe, beforeAll (remove or repurpose) and the fetch call
accordingly.
In `@tests/llm-eval/deepeval.config.ts`:
- Around line 1-12: The header comment lists "Bias detection" and "Toxicity" as
evaluated items but the implementation is missing; either implement those
evaluators or mark them as TODO in the top comment. Update the header block that
begins "DeepEval LLM Evaluation Configuration" to either remove those items, add
TODO markers ("TODO: implement bias detection, toxicity checks") or wire up
actual evaluator modules/entries in the DeepEval configuration (ensure the
config/export that defines evaluators includes modules for bias/toxicity with
clear names like biasEvaluator and toxicityEvaluator). Make the change so the
comment accurately reflects what's implemented and the config references the
corresponding evaluator identifiers.
- Around line 150-176: The hallucination logic is inverted and cases without
context always fail: change the hallucination computation to detect extra info
in actualOutput not present in testCase.context (e.g., compute extraTokens or
extraPhrases by checking words/phrases in actualOutput that are not found in
testCase.context) and set result.scores.hallucinationRate = extraCount /
(wordCount || 1); only compute result.scores.contextCoverage when
testCase.context && testCase.context.length > 0 (keep contextHits logic) and if
no context is provided, skip contextCoverage/hallucinationRate checks (or set
them to null/undefined) so they don’t force failure; finally update
result.passed to rely on hasSubstantiveContent and, when context exists, require
contextCoverage >= 0.5 as well (use the existing contextCoverage and hasContent
variables).
In `@tests/unit/llm-ollama.test.ts`:
- Around line 92-108: In the 'should handle system prompt' test in
tests/unit/llm-ollama.test.ts (the test that calls fetch to
`${OLLAMA_BASE_URL}/api/generate` with model OLLAMA_MODEL and expects an
OllamaResponse), change the options.temperature from 0.7 to 0 so the call is
deterministic; update the JSON body sent in that test (the object containing
model, prompt, stream, options) to use temperature: 0 to match other tests and
ensure the regex assertion remains stable.
- Around line 65-89: The test "should generate JSON format" currently asserts
response.ok but doesn't handle older Ollama versions that return HTTP errors for
the unsupported format parameter; update the test in
tests/unit/llm-ollama.test.ts (inside the it block) to check response.ok and, if
false, return early to skip the rest of the assertions (gracefully exit the
test) before attempting to parse data.response or run JSON assertions against
OllamaResponse; keep the existing try/catch JSON.parse logic for successful
responses.
In `@tests/unit/supabase.test.ts`:
- Around line 15-20: Remove the hardcoded demo JWT by eliminating the fallback
value for SUPABASE_ANON_KEY so it only reads
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, add a conditional test wrapper (e.g.,
describeIfSupabase) that checks presence of SUPABASE_ANON_KEY and calls describe
or describe.skip accordingly, and replace the top-level describe usage for the
"Schema Validation" suite with describeIfSupabase(SUPABASE_ANON_KEY, 'Schema
Validation', ...) to skip when the key is missing; also apply the same
describeIfSupabase wrapper to the "RLS Policy Validation" suite (leave the "Chat
Service Integration" suite unchanged since it uses mocks).
🧹 Nitpick comments (4)
tests/unit/mcp-rag-tools.test.ts (1)
238-246: Consider verifying the error message for the validation failure.The test correctly validates that an empty title returns
success: false, but doesn't verify the error message. For validation tests, asserting on the error content helps catch regressions and ensures meaningful feedback is provided.Additionally, line 245 is redundant—if
result.successequalsfalse, the property inherently exists.♻️ Suggested improvement
// Tool should return error for empty title, not throw const result = await tool.execute({ title: '', content: 'Content', docType: 'policy', }, null); - expect(result).toHaveProperty('success'); expect(result.success).toBe(false); + expect(result.error).toContain('title');tests/llm-eval/deepeval.config.ts (2)
272-285:averageScorecurrently equals average context coverage only.If the intent is a weighted evaluation score, consider incorporating other metrics (e.g., hallucination/relevancy/faithfulness) or rename the output to avoid confusion.
205-230: Add timeout and response shape validation for the Ollama API call.The current implementation lacks a timeout mechanism, which means hung requests can stall batch evaluation runs indefinitely. Additionally,
data.responseis accessed without validation—while the Ollama API guarantees this field in non-streaming mode, defensive validation prevents silent failures from unexpected payloads.⏱️ Suggested hardening
export async function generateLLMResponse( testCase: EvaluationTestCase ): Promise<string> { const prompt = generateTestPrompt(testCase); + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30_000); - const response = await fetch(`${EVAL_LLM_CONFIG.baseUrl}/api/generate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: EVAL_LLM_CONFIG.model, - prompt, - stream: false, - options: { - temperature: EVAL_LLM_CONFIG.temperature, - num_predict: EVAL_LLM_CONFIG.maxTokens, - }, - }), - }); + let response: Response; + try { + response = await fetch(`${EVAL_LLM_CONFIG.baseUrl}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + signal: controller.signal, + body: JSON.stringify({ + model: EVAL_LLM_CONFIG.model, + prompt, + stream: false, + options: { + temperature: EVAL_LLM_CONFIG.temperature, + num_predict: EVAL_LLM_CONFIG.maxTokens, + }, + }), + }); + } finally { + clearTimeout(timeout); + } if (!response.ok) { throw new Error(`LLM request failed: ${response.statusText}`); } const data = await response.json(); - return data.response; + if (typeof data?.response !== 'string') { + throw new Error('LLM response payload missing "response" string.'); + } + return data.response; }mocks/mock-server.js (1)
54-57: Avoid relying onbtoafor Node compatibility.
btoaisn’t available in all supported Node versions. UsingBufferavoids runtime surprises.🔧 Safer token helper
-function jwt() { - return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + btoa(JSON.stringify({ exp: Date.now() + 3600000 })); -} +function jwt() { + const payload = Buffer.from(JSON.stringify({ exp: Date.now() + 3600000 })).toString('base64url'); + return `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${payload}`; +}
| if (path === '/api/auth/login' && method === 'POST') { | ||
| let body = ''; | ||
| req.on('data', chunk => body += chunk); | ||
| req.on('end', () => { | ||
| const { email } = JSON.parse(body); | ||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify({ | ||
| access_token: jwt(), | ||
| refresh_token: uuid(), | ||
| user: { id: uuid(), email: email || 'test@example.com', role: 'customer' }, | ||
| session_id: uuid() | ||
| })); | ||
| }); | ||
| return; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd mock-server.jsRepository: Aparnap2/smart_commerce_agent
Length of output: 93
🏁 Script executed:
wc -l mocks/mock-server.jsRepository: Aparnap2/smart_commerce_agent
Length of output: 97
🏁 Script executed:
cat -n mocks/mock-server.js | sed -n '80,110p'Repository: Aparnap2/smart_commerce_agent
Length of output: 1298
🏁 Script executed:
cat -n mocks/mock-server.js | sed -n '81,160p'Repository: Aparnap2/smart_commerce_agent
Length of output: 3541
🏁 Script executed:
cat -n mocks/mock-server.js | sed -n '340,351p'Repository: Aparnap2/smart_commerce_agent
Length of output: 524
🏁 Script executed:
cat -n mocks/mock-server.js | sed -n '310,351p'Repository: Aparnap2/smart_commerce_agent
Length of output: 1860
🏁 Script executed:
rg 'JSON\.parse' mocks/mock-server.js -nRepository: Aparnap2/smart_commerce_agent
Length of output: 291
🏁 Script executed:
cat -n mocks/mock-server.js | sed -n '165,215p'Repository: Aparnap2/smart_commerce_agent
Length of output: 2179
🏁 Script executed:
cat -n mocks/mock-server.js | sed -n '260,280p'Repository: Aparnap2/smart_commerce_agent
Length of output: 987
🏁 Script executed:
cat -n mocks/mock-server.js | sed -n '1,85p'Repository: Aparnap2/smart_commerce_agent
Length of output: 3596
Add try-catch guards around JSON.parse in async request handlers.
JSON.parse errors inside req.on('end') callbacks escape the outer try-catch block (which only covers synchronous route matching). This affects 5 endpoints: /api/auth/login, /api/tickets (POST/PUT), /api/tickets/:id/messages, and /api/webhooks/stripe. Add error handling to each callback:
- const { email } = JSON.parse(body);
+ let email = 'test@example.com';
+ try {
+ ({ email } = JSON.parse(body));
+ } catch {
+ res.writeHead(400, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
+ return;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (path === '/api/auth/login' && method === 'POST') { | |
| let body = ''; | |
| req.on('data', chunk => body += chunk); | |
| req.on('end', () => { | |
| const { email } = JSON.parse(body); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| access_token: jwt(), | |
| refresh_token: uuid(), | |
| user: { id: uuid(), email: email || 'test@example.com', role: 'customer' }, | |
| session_id: uuid() | |
| })); | |
| }); | |
| return; | |
| if (path === '/api/auth/login' && method === 'POST') { | |
| let body = ''; | |
| req.on('data', chunk => body += chunk); | |
| req.on('end', () => { | |
| let email = 'test@example.com'; | |
| try { | |
| ({ email } = JSON.parse(body)); | |
| } catch { | |
| res.writeHead(400, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ error: 'Invalid JSON' })); | |
| return; | |
| } | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| access_token: jwt(), | |
| refresh_token: uuid(), | |
| user: { id: uuid(), email: email || 'test@example.com', role: 'customer' }, | |
| session_id: uuid() | |
| })); | |
| }); | |
| return; |
🤖 Prompt for AI Agents
In `@mocks/mock-server.js` around lines 90 - 103, The async request handlers for
routes '/api/auth/login', '/api/tickets' (POST/PUT),
'/api/tickets/:id/messages', and '/api/webhooks/stripe' call JSON.parse inside
the req.on('end') callback without try/catch; wrap the JSON.parse and subsequent
payload handling in a try/catch within each req.on('end') listener (the
anonymous callback passed to req.on('end')) and on parse error reply with a 400
response (res.writeHead(400) / res.end with an error JSON) so the server does
not crash and the endpoint returns a proper client error.
| OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}" | ||
| SUPABASE_URL="${SUPABASE_URL:-http://localhost:8000}" | ||
| MOCKOON_URL="${MOCKOON_URL:-http://localhost:3000}" | ||
| NEXTJS_URL="${NEXTJS_URL:-http://localhost:3000}" | ||
| API_URL="${API_URL:-http://localhost:3000/api}" |
There was a problem hiding this comment.
Default Mockoon/Next.js ports collide.
Both defaults point to 3000, so one service will always fail the health check unless users override envs. Consider giving Mockoon a distinct default (or making one derive from the other).
💡 Suggested default split
-MOCKOON_URL="${MOCKOON_URL:-http://localhost:3000}"
-NEXTJS_URL="${NEXTJS_URL:-http://localhost:3000}"
+MOCKOON_URL="${MOCKOON_URL:-http://localhost:3001}"
+NEXTJS_URL="${NEXTJS_URL:-http://localhost:3000}"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}" | |
| SUPABASE_URL="${SUPABASE_URL:-http://localhost:8000}" | |
| MOCKOON_URL="${MOCKOON_URL:-http://localhost:3000}" | |
| NEXTJS_URL="${NEXTJS_URL:-http://localhost:3000}" | |
| API_URL="${API_URL:-http://localhost:3000/api}" | |
| OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}" | |
| SUPABASE_URL="${SUPABASE_URL:-http://localhost:8000}" | |
| MOCKOON_URL="${MOCKOON_URL:-http://localhost:3001}" | |
| NEXTJS_URL="${NEXTJS_URL:-http://localhost:3000}" | |
| API_URL="${API_URL:-http://localhost:3000/api}" |
🤖 Prompt for AI Agents
In `@scripts/health-check.sh` around lines 17 - 21, The health-check defaults
collide because MOCKOON_URL and NEXTJS_URL both default to port 3000; update the
environment default for MOCKOON_URL (or derive it from NEXTJS_URL) so services
use distinct ports — e.g., change the MOCKOON_URL default from
"http://localhost:3000" to a different port like "http://localhost:3001" (or
programmatically set
MOCKOON_URL="${MOCKOON_URL:-${NEXTJS_URL/http:\/\/localhost:3000/http:\/\/localhost:3001}}")
so that MOCKOON_URL and NEXTJS_URL do not conflict in the health-check script.
| # Test LLM generation | ||
| log_info "Testing LLM generation..." | ||
| RESPONSE=$(curl -s -X POST "$OLLAMA_URL/api/generate" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{ | ||
| "model":"qwen2.5-coder:3b", | ||
| "prompt":"Say exactly: Hello from Ollama!", | ||
| "stream":false, | ||
| "options":{"temperature":0} | ||
| }') | ||
|
|
||
| if echo "$RESPONSE" | grep -q "Hello from Ollama!"; then | ||
| log_success "LLM generation working correctly" | ||
| else | ||
| log_fail "LLM generation failed" | ||
| echo "Response: $RESPONSE" | head -c 200 | ||
| fi | ||
|
|
||
| # Test embedding generation | ||
| log_info "Testing embedding generation..." | ||
| EMBEDDING=$(curl -s -X POST "$OLLAMA_URL/api/embed" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"model":"nomic-embed-text","prompt":"test embedding"}') | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat scripts/health-check.shRepository: Aparnap2/smart_commerce_agent
Length of output: 9403
🏁 Script executed:
grep -n 'RESPONSE=\|EMBEDDING=' scripts/health-check.shRepository: Aparnap2/smart_commerce_agent
Length of output: 432
🏁 Script executed:
# Test bash behavior with set -e and command substitution failures
python3 << 'EOF'
# Demonstrating the issue with set -e and command substitutions
test_script = """#!/bin/bash
set -e
echo "Before command substitution"
RESPONSE=$(false) # This will fail
echo "After command substitution"
"""
print("Bash behavior with 'set -e':")
print("- When RESPONSE=$(false) is executed with 'set -e', the script terminates")
print("- The non-zero exit status from the command substitution causes set -e to exit")
print("- Without a guard like '|| true', there's no recovery")
print()
print("This means in the script:")
print("- Lines 86, 104, 139: curl failures terminate the script immediately")
print("- Prevents log_fail() calls and summary from executing")
print()
print("With '|| true' guard:")
print("- RESPONSE=$(curl ... || true)")
print("- Returns 0 even if curl fails, allowing error handling via if/grep checks")
EOFRepository: Aparnap2/smart_commerce_agent
Length of output: 588
🏁 Script executed:
# Check all curl command substitutions and their surrounding context
grep -B 1 -A 5 'RESPONSE=$(curl\|EMBEDDING=$(curl\|MOCK_RESPONSE=$(curl\|CHAT_RESPONSE=$(curl' scripts/health-check.sh | head -60Repository: Aparnap2/smart_commerce_agent
Length of output: 1636
Guard curl command substitutions to prevent early termination on network failures.
With set -e enabled, a curl connection failure exits the script immediately without logging the failure or printing the summary. Add || true to the three unguarded curl assignments (lines 86, 104, 139) to allow the error-handling logic below each assignment to execute properly.
🛡️ Guard the substitutions
- RESPONSE=$(curl -s -X POST "$OLLAMA_URL/api/generate" \
+ RESPONSE=$(curl -s -X POST "$OLLAMA_URL/api/generate" \
-H "Content-Type: application/json" \
-d '{
"model":"qwen2.5-coder:3b",
"prompt":"Say exactly: Hello from Ollama!",
"stream":false,
"options":{"temperature":0}
- }')
+ }' || true)
@@
- EMBEDDING=$(curl -s -X POST "$OLLAMA_URL/api/embed" \
+ EMBEDDING=$(curl -s -X POST "$OLLAMA_URL/api/embed" \
-H "Content-Type: application/json" \
- -d '{"model":"nomic-embed-text","prompt":"test embedding"}')
+ -d '{"model":"nomic-embed-text","prompt":"test embedding"}' || true)
@@
- RESPONSE=$(curl -s "$SUPABASE_URL/rest/v1/organizations?select=count" \
+ RESPONSE=$(curl -s "$SUPABASE_URL/rest/v1/organizations?select=count" \
-H "apikey: $SUPABASE_ANON_KEY" \
- -H "Authorization: Bearer $SUPABASE_ANON_KEY")
+ -H "Authorization: Bearer $SUPABASE_ANON_KEY" || true)🤖 Prompt for AI Agents
In `@scripts/health-check.sh` around lines 84 - 107, The curl command
substitutions used to set RESPONSE and EMBEDDING (and the third unshown curl at
the other location) can cause the script to exit immediately under set -e on
network failure, so update the assignments that call curl (the ones assigning
RESPONSE and EMBEDDING and the third curl assignment) to append || true to the
command substitution to prevent early termination and allow the subsequent
error-handling (the if/else checks and log_fail/log_info) to run; ensure you
only change how the curl substitutions are invoked (add || true) and do not
alter the subsequent checks that inspect RESPONSE or EMBEDDING.
| # Load environment variables | ||
| if [ -f .env.test.docker ]; then | ||
| export $(cat .env.test.docker | grep -v '^#' | xargs) | ||
| fi |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and examine lines 273-276
if [ -f scripts/health-check.sh ]; then
echo "=== File found. Examining lines 273-276 ==="
sed -n '273,276p' scripts/health-check.sh
echo ""
echo "=== Full context around those lines ==="
sed -n '270,280p' scripts/health-check.sh
else
echo "File not found. Searching for health-check.sh"
find . -name "health-check.sh" -type f
fiRepository: Aparnap2/smart_commerce_agent
Length of output: 595
🏁 Script executed:
# Install and run shellcheck on the file
if ! command -v shellcheck &> /dev/null; then
echo "Installing shellcheck..."
apt-get update -qq && apt-get install -y -qq shellcheck > /dev/null 2>&1
fi
echo "=== Running shellcheck on scripts/health-check.sh ==="
shellcheck -f gcc scripts/health-check.sh | grep -A2 -B2 "273\|274\|275\|276"
echo ""
echo "=== Checking all SC2046 warnings in the file ==="
shellcheck -f gcc scripts/health-check.sh | grep SC2046
echo ""
echo "=== Full shellcheck output for context ==="
shellcheck -f gcc scripts/health-check.sh | head -50Repository: Aparnap2/smart_commerce_agent
Length of output: 484
🏁 Script executed:
# Create a temporary test file with the suggested fix
cat > /tmp/test_health_check.sh << 'EOF'
#!/bin/bash
# This is the suggested fix
if [ -f .env.test.docker ]; then
set -a
# shellcheck disable=SC1091
source .env.test.docker
set +a
fi
EOF
echo "=== Testing the suggested fix with shellcheck ==="
shellcheck -f gcc /tmp/test_health_check.sh
if [ $? -eq 0 ]; then
echo "(No warnings - fix resolves the issue)"
else
echo "Still has warnings"
fi
echo ""
echo "=== Testing with the original code pattern ==="
cat > /tmp/test_original.sh << 'EOF'
#!/bin/bash
if [ -f .env.test.docker ]; then
export $(cat .env.test.docker | grep -v '^#' | xargs)
fi
EOF
shellcheck -f gcc /tmp/test_original.shRepository: Aparnap2/smart_commerce_agent
Length of output: 293
Load .env.test.docker safely to avoid word-splitting.
The current export pipeline breaks on values with spaces and triggers SC2046. Use source with set -a/set +a to safely load environment variables:
🧩 Safer env loading
- if [ -f .env.test.docker ]; then
- export $(cat .env.test.docker | grep -v '^#' | xargs)
- fi
+ if [ -f .env.test.docker ]; then
+ set -a
+ # shellcheck disable=SC1091
+ source .env.test.docker
+ set +a
+ fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Load environment variables | |
| if [ -f .env.test.docker ]; then | |
| export $(cat .env.test.docker | grep -v '^#' | xargs) | |
| fi | |
| # Load environment variables | |
| if [ -f .env.test.docker ]; then | |
| set -a | |
| # shellcheck disable=SC1091 | |
| source .env.test.docker | |
| set +a | |
| fi |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 275-275: Quote this to prevent word splitting.
(SC2046)
🤖 Prompt for AI Agents
In `@scripts/health-check.sh` around lines 273 - 276, Replace the current export
pipeline that uses cat/grep/xargs with a safe sourcing approach: inside the
existing if [ -f .env.test.docker ]; then ... fi block, use "set -a; .
.env.test.docker; set +a" (or "set -a; source .env.test.docker; set +a") so
variables from .env.test.docker are exported without word-splitting; ensure the
conditional (the if [ -f .env.test.docker ]; then) and the file name
".env.test.docker" remain the same.
| import { describe, it, expect, beforeAll } from 'vitest'; | ||
|
|
||
| // Configuration | ||
| const MOCKOON_URL = process.env.MOCKOON_URL || 'http://localhost:3000'; | ||
| const API_BASE = MOCKOON_URL; | ||
|
|
||
| let mockoonAvailable = false; | ||
|
|
||
| beforeAll(async () => { | ||
| try { | ||
| const response = await fetch(API_BASE, { method: 'HEAD', timeout: 2000 }); | ||
| mockoonAvailable = response.ok; | ||
| } catch { | ||
| mockoonAvailable = false; | ||
| } | ||
| }); | ||
|
|
||
| const conditionalDescribe = mockoonAvailable ? describe : describe.skip; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n tests/e2e/workflow.test.ts | head -50Repository: Aparnap2/smart_commerce_agent
Length of output: 1905
🏁 Script executed:
# Check if there's any health endpoint defined
rg -i "health|/api/" --max-count 20 | head -30Repository: Aparnap2/smart_commerce_agent
Length of output: 2875
🌐 Web query:
Does fetch() support timeout option in JavaScript
💡 Result:
No—the standard fetch(url, options) API does not have a timeout option (there’s an open/proposed discussion in the Fetch Standard repo, but it’s not part of the spec/options today). [1]
To enforce time limits, you use abort signals:
- Preferred (where supported):
AbortSignal.timeout(ms)withfetch’ssignaloption. [2] - Fallback: create an
AbortController()and callcontroller.abort()fromsetTimeout, passingcontroller.signaltofetch. [3]
Note: some libraries (e.g., older node-fetch) added a non-standard timeout option, but it’s not portable across real fetch implementations. [4]
Sources: [1] [2] [3] [4]
🏁 Script executed:
# Check package.json and tsconfig for module/ES support
cat package.json | grep -A 10 '"type"'
head -20 tsconfig.json | grep -E "module|target|moduleResolution"Repository: Aparnap2/smart_commerce_agent
Length of output: 373
🏁 Script executed:
# Double-check if test files use top-level await patterns elsewhere
rg "await.*async" tests/e2e/ tests/unit/ --max-count 5Repository: Aparnap2/smart_commerce_agent
Length of output: 55
Fix test gating logic to actually evaluate Mockoon availability.
The beforeAll hook is registered after conditionalDescribe is evaluated at module load time, when mockoonAvailable is still false. The test suite will always be skipped. Additionally, fetch doesn't support the timeout option—use AbortController with AbortSignal instead. Move the availability check to the module top level:
Deterministic gating with AbortController
-import { describe, it, expect, beforeAll } from 'vitest';
+import { describe, it, expect } from 'vitest';
@@
-let mockoonAvailable = false;
-
-beforeAll(async () => {
- try {
- const response = await fetch(API_BASE, { method: 'HEAD', timeout: 2000 });
- mockoonAvailable = response.ok;
- } catch {
- mockoonAvailable = false;
- }
-});
-
-const conditionalDescribe = mockoonAvailable ? describe : describe.skip;
+const mockoonAvailable = await (async () => {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), 2000);
+ try {
+ const response = await fetch(`${API_BASE}/api/health`, {
+ method: 'GET',
+ signal: controller.signal,
+ });
+ return response.ok;
+ } catch {
+ return false;
+ } finally {
+ clearTimeout(timeout);
+ }
+})();
+
+const conditionalDescribe = mockoonAvailable ? describe : describe.skip;🤖 Prompt for AI Agents
In `@tests/e2e/workflow.test.ts` around lines 11 - 29, The test gating uses
conditionalDescribe computed at module load while mockoonAvailable is still
false and also incorrectly passes a nonexistent timeout option to fetch; fix by
performing the availability check at module top-level before computing
conditionalDescribe (use top-level await to HEAD API_BASE), use
AbortController/AbortSignal to implement a timeout for the fetch, set
mockoonAvailable based on response.ok, then compute conditionalDescribe =
mockoonAvailable ? describe : describe.skip so the suite is gated
deterministically; update references to mockoonAvailable, conditionalDescribe,
beforeAll (remove or repurpose) and the fetch call accordingly.
| /** | ||
| * DeepEval LLM Evaluation Configuration | ||
| * | ||
| * Evaluates LLM responses for: | ||
| * - Hallucination detection | ||
| * - Answer relevancy | ||
| * - Faithfulness | ||
| * - Bias detection | ||
| * - Toxicity | ||
| * | ||
| * Uses qwen2.5-coder:3b with Ollama | ||
| */ |
There was a problem hiding this comment.
Header comment promises bias/toxicity checks that aren’t implemented.
Either implement those evaluators or mark them as TODO to avoid misleading readers.
✍️ Suggested comment tweak
- * - Bias detection
- * - Toxicity
+ * - (TODO) Bias detection
+ * - (TODO) Toxicity📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * DeepEval LLM Evaluation Configuration | |
| * | |
| * Evaluates LLM responses for: | |
| * - Hallucination detection | |
| * - Answer relevancy | |
| * - Faithfulness | |
| * - Bias detection | |
| * - Toxicity | |
| * | |
| * Uses qwen2.5-coder:3b with Ollama | |
| */ | |
| /** | |
| * DeepEval LLM Evaluation Configuration | |
| * | |
| * Evaluates LLM responses for: | |
| * - Hallucination detection | |
| * - Answer relevancy | |
| * - Faithfulness | |
| * - (TODO) Bias detection | |
| * - (TODO) Toxicity | |
| * | |
| * Uses qwen2.5-coder:3b with Ollama | |
| */ |
🤖 Prompt for AI Agents
In `@tests/llm-eval/deepeval.config.ts` around lines 1 - 12, The header comment
lists "Bias detection" and "Toxicity" as evaluated items but the implementation
is missing; either implement those evaluators or mark them as TODO in the top
comment. Update the header block that begins "DeepEval LLM Evaluation
Configuration" to either remove those items, add TODO markers ("TODO: implement
bias detection, toxicity checks") or wire up actual evaluator modules/entries in
the DeepEval configuration (ensure the config/export that defines evaluators
includes modules for bias/toxicity with clear names like biasEvaluator and
toxicityEvaluator). Make the change so the comment accurately reflects what's
implemented and the config references the corresponding evaluator identifiers.
| // Check if output contains expected information | ||
| if (testCase.context && testCase.context.length > 0) { | ||
| const contextHits = testCase.context.filter((ctx) => | ||
| actualOutput.toLowerCase().includes(ctx.toLowerCase()) | ||
| ); | ||
| result.scores.contextCoverage = contextHits.length / testCase.context.length; | ||
| } | ||
|
|
||
| // Check for hallucinations (output contains info not in context) | ||
| if (testCase.context) { | ||
| const hallucinations = testCase.context.filter( | ||
| (ctx) => !actualOutput.toLowerCase().includes(ctx.toLowerCase()) | ||
| ); | ||
| // Simple check - in production use LLM-based evaluation | ||
| result.scores.hallucinationRate = hallucinations.length / (testCase.context.length || 1); | ||
| } | ||
|
|
||
| // Check answer relevance (length and coherence) | ||
| const wordCount = actualOutput.split(/\s+/).length; | ||
| result.scores.wordCount = wordCount; | ||
| result.scores.hasSubstantiveContent = wordCount >= 5 && wordCount <= 500; | ||
|
|
||
| // Overall pass/fail | ||
| const contextCoverage = result.scores.contextCoverage as number || 0; | ||
| const hasContent = result.scores.hasSubstantiveContent as boolean || false; | ||
|
|
||
| result.passed = contextCoverage >= 0.5 && hasContent; |
There was a problem hiding this comment.
Hallucination rate is inverted, and context-less cases always fail.
The current logic counts missing context as hallucinations, which is the opposite of “extra info not in context.” Also, when context is absent, contextCoverage defaults to 0 and the case fails even if the response is good.
🛠️ Suggested fix
- // Check for hallucinations (output contains info not in context)
- if (testCase.context) {
- const hallucinations = testCase.context.filter(
- (ctx) => !actualOutput.toLowerCase().includes(ctx.toLowerCase())
- );
- // Simple check - in production use LLM-based evaluation
- result.scores.hallucinationRate = hallucinations.length / (testCase.context.length || 1);
- }
+ // Check for hallucinations (output contains info not in context)
+ if (testCase.context && testCase.context.length > 0) {
+ const contextLower = testCase.context.map((ctx) => ctx.toLowerCase());
+ const sentences = actualOutput.split(/[.!?]\s+/).filter(Boolean);
+ const hallucinated = sentences.filter(
+ (sentence) => !contextLower.some((ctx) => sentence.toLowerCase().includes(ctx))
+ );
+ result.scores.hallucinationRate = hallucinated.length / (sentences.length || 1);
+ }
- const contextCoverage = result.scores.contextCoverage as number || 0;
+ const hasContext = (testCase.context?.length ?? 0) > 0;
+ const contextCoverage = hasContext ? (result.scores.contextCoverage as number || 0) : 1;
const hasContent = result.scores.hasSubstantiveContent as boolean || false;
- result.passed = contextCoverage >= 0.5 && hasContent;
+ result.passed = (!hasContext || contextCoverage >= 0.5) && hasContent;🤖 Prompt for AI Agents
In `@tests/llm-eval/deepeval.config.ts` around lines 150 - 176, The hallucination
logic is inverted and cases without context always fail: change the
hallucination computation to detect extra info in actualOutput not present in
testCase.context (e.g., compute extraTokens or extraPhrases by checking
words/phrases in actualOutput that are not found in testCase.context) and set
result.scores.hallucinationRate = extraCount / (wordCount || 1); only compute
result.scores.contextCoverage when testCase.context && testCase.context.length >
0 (keep contextHits logic) and if no context is provided, skip
contextCoverage/hallucinationRate checks (or set them to null/undefined) so they
don’t force failure; finally update result.passed to rely on
hasSubstantiveContent and, when context exists, require contextCoverage >= 0.5
as well (use the existing contextCoverage and hasContent variables).
| it('should generate JSON format', async () => { | ||
| const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| model: OLLAMA_MODEL, | ||
| prompt: 'Return a JSON object with fields: name (string), age (number), active (boolean)', | ||
| stream: false, | ||
| format: 'json', | ||
| options: { temperature: 0 }, | ||
| }), | ||
| }); | ||
|
|
||
| expect(response.ok).toBe(true); | ||
| const data: OllamaResponse = await response.json(); | ||
|
|
||
| try { | ||
| const parsed = JSON.parse(data.response); | ||
| expect(parsed).toHaveProperty('name'); | ||
| expect(parsed).toHaveProperty('age'); | ||
| expect(parsed).toHaveProperty('active'); | ||
| } catch { | ||
| // Some Ollama versions don't support format parameter | ||
| expect(typeof data.response).toBe('string'); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /root && find . -type f -name "llm-ollama.test.ts" | head -20Repository: Aparnap2/smart_commerce_agent
Length of output: 128
🏁 Script executed:
cd /root && git ls-files | grep -E "(test|llm)" | head -30Repository: Aparnap2/smart_commerce_agent
Length of output: 128
🏁 Script executed:
find . -type f -name "llm-ollama.test.ts" 2>/dev/nullRepository: Aparnap2/smart_commerce_agent
Length of output: 104
🏁 Script executed:
git ls-files | grep -E "(llm|ollama)" | head -20Repository: Aparnap2/smart_commerce_agent
Length of output: 528
🏁 Script executed:
git ls-files | grep "\.test\.ts$" | head -20Repository: Aparnap2/smart_commerce_agent
Length of output: 1410
🏁 Script executed:
cat -n tests/unit/llm-ollama.test.tsRepository: Aparnap2/smart_commerce_agent
Length of output: 11221
🌐 Web query:
Ollama API format parameter json support version compatibility error handling
💡 Result:
format parameter: JSON mode + JSON schema (native Ollama API)
POST /api/generate:formatsupports either the string"json"or a JSON Schema object. Use"stream": falseif you want a single JSON response blob. [1]POST /api/chat: same:formatcan be"json"or a JSON Schema object. [2]- Ollama documents structured outputs (JSON mode and schema-constrained outputs) as a first-class capability. [3]
- Versioning note: Ollama states the API “isn’t strictly versioned” but is intended to be stable/backwards compatible. [4]
OpenAI-compatible endpoints (/v1/...): use response_format, not format
- For
/v1/chat/completions, Ollama listsresponse_formatas a supported request field and “JSON mode” as a supported feature. [5] - For
/v1/responses, Ollama notes it was added in Ollama v0.13.3. [6]
Compatibility gotchas (real-world issues)
- Old versions may misbehave with
format: "json": a GitHub issue reports requests hanging when"format": "json"is set (reported on Ollama 0.1.17–0.1.19). If you see this, upgrade Ollama and/or switch to schema-based structured outputs. [7] response_format: {type:"json_schema", ...}may be ignored on the OpenAI-compatible endpoint in some versions (reported on Ollama 0.6.2); the reporter notes Ollama’s native API expects schema viaformatinstead. [8]
Error handling (including streaming)
- Ollama returns appropriate HTTP codes (e.g., 400 invalid JSON/missing params, 404 missing model, 429, 5xx) and a JSON body shaped like:
{"error":"..."}. [9] - Streaming error case: if an error happens mid-stream, Ollama emits an NDJSON line containing
{"error":"..."}and the HTTP status won’t change because the response already started—so your stream parser must watch for anerrorobject. [9]
Practical client checks
- Always validate
Content-Type(application/jsonvsapplication/x-ndjson), and when streaming parse line-by-line and treat any line containingerroras terminal. [9] - For JSON mode (
format:"json"/ OpenAIresponse_format), still instruct the model to output JSON; otherwise you can get invalid JSON or runaway whitespace (Ollama explicitly warns about this). [3]
Sources:
[1] docs.ollama.com/api/generate
[2] docs.ollama.com/api/chat
[3] docs.ollama.com/capabilities/structured-outputs
[4] docs.ollama.com/api/introduction
[5] docs.ollama.com/api/openai-compatibility
[6] docs.ollama.com/api/openai-compatibility
[7] github.com/ollama/ollama/issues/1910
[8] github.com/ollama/ollama/issues/10001
[9] docs.ollama.com/api/errors
Handle Ollama versions that reject format: "json" parameter.
The test currently requires response.ok before processing the response, but doesn't account for older Ollama versions (0.1.17–0.1.19) that reject the unsupported format parameter with HTTP errors. Add an early return when the server rejects the request to skip the test gracefully instead of failing the assertion.
Example fix
expect(response.ok).toBe(true);
+ if (!response.ok) {
+ console.log('[Test] Skipping - format parameter not supported by this Ollama version');
+ return;
+ }
const data: OllamaResponse = await response.json();🤖 Prompt for AI Agents
In `@tests/unit/llm-ollama.test.ts` around lines 65 - 89, The test "should
generate JSON format" currently asserts response.ok but doesn't handle older
Ollama versions that return HTTP errors for the unsupported format parameter;
update the test in tests/unit/llm-ollama.test.ts (inside the it block) to check
response.ok and, if false, return early to skip the rest of the assertions
(gracefully exit the test) before attempting to parse data.response or run JSON
assertions against OllamaResponse; keep the existing try/catch JSON.parse logic
for successful responses.
| it('should handle system prompt', async () => { | ||
| const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| model: OLLAMA_MODEL, | ||
| prompt: 'You are a pirate. Say hello.', | ||
| stream: false, | ||
| options: { temperature: 0.7 }, | ||
| }), | ||
| }); | ||
|
|
||
| expect(response.ok).toBe(true); | ||
| const data: OllamaResponse = await response.json(); | ||
| expect(data.response).toBeDefined(); | ||
| expect(data.response.toLowerCase()).toMatch(/ahoy|pirate|shiver/); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n tests/unit/llm-ollama.test.ts | sed -n '85,115p'Repository: Aparnap2/smart_commerce_agent
Length of output: 1373
🏁 Script executed:
cat -n tests/unit/llm-ollama.test.ts | head -100Repository: Aparnap2/smart_commerce_agent
Length of output: 4106
Use temperature: 0 for deterministic test behavior.
This test is the only one in the suite using temperature: 0.7; all other tests use temperature: 0 for reproducible results. The pirate prompt at higher temperature produces variable outputs that can fail the strict regex assertion /ahoy|pirate|shiver/.
Suggested fix
- options: { temperature: 0.7 },
+ options: { temperature: 0 },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('should handle system prompt', async () => { | |
| const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| model: OLLAMA_MODEL, | |
| prompt: 'You are a pirate. Say hello.', | |
| stream: false, | |
| options: { temperature: 0.7 }, | |
| }), | |
| }); | |
| expect(response.ok).toBe(true); | |
| const data: OllamaResponse = await response.json(); | |
| expect(data.response).toBeDefined(); | |
| expect(data.response.toLowerCase()).toMatch(/ahoy|pirate|shiver/); | |
| }); | |
| it('should handle system prompt', async () => { | |
| const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| model: OLLAMA_MODEL, | |
| prompt: 'You are a pirate. Say hello.', | |
| stream: false, | |
| options: { temperature: 0 }, | |
| }), | |
| }); | |
| expect(response.ok).toBe(true); | |
| const data: OllamaResponse = await response.json(); | |
| expect(data.response).toBeDefined(); | |
| expect(data.response.toLowerCase()).toMatch(/ahoy|pirate|shiver/); | |
| }); |
🤖 Prompt for AI Agents
In `@tests/unit/llm-ollama.test.ts` around lines 92 - 108, In the 'should handle
system prompt' test in tests/unit/llm-ollama.test.ts (the test that calls fetch
to `${OLLAMA_BASE_URL}/api/generate` with model OLLAMA_MODEL and expects an
OllamaResponse), change the options.temperature from 0.7 to 0 so the call is
deterministic; update the JSON body sent in that test (the object containing
model, prompt, stream, options) to use temperature: 0 to match other tests and
ensure the regex assertion remains stable.
| const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || 'http://localhost:8000'; | ||
| const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE'; | ||
|
|
||
| describe('Supabase Integration', () => { | ||
| describe('Schema Validation', () => { | ||
| it('should have organizations table', async () => { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the full test file to understand its structure
wc -l tests/unit/supabase.test.tsRepository: Aparnap2/smart_commerce_agent
Length of output: 104
🏁 Script executed:
# Read the entire test file
cat -n tests/unit/supabase.test.tsRepository: Aparnap2/smart_commerce_agent
Length of output: 10259
🏁 Script executed:
# Decode the JWT to understand if it's a real secret or demo token
python3 << 'EOF'
import base64
import json
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
# Split the JWT
parts = jwt.split('.')
if len(parts) == 3:
header = parts[0]
payload = parts[1]
signature = parts[2]
# Add padding if needed
padding = 4 - len(payload) % 4
if padding != 4:
payload += '=' * padding
try:
decoded_payload = json.loads(base64.urlsafe_b64decode(payload))
print("JWT Payload:")
print(json.dumps(decoded_payload, indent=2))
print("\nAnalysis:")
print(f"- Issuer: {decoded_payload.get('iss')}")
print(f"- Role: {decoded_payload.get('role')}")
print(f"- Is demo token: {'demo' in decoded_payload.get('iss', '').lower()}")
except Exception as e:
print(f"Error: {e}")
else:
print("Invalid JWT format")
EOFRepository: Aparnap2/smart_commerce_agent
Length of output: 244
Remove the hardcoded Supabase demo key and skip suites when missing.
Even demo JWTs should not be committed to source control—they will be flagged by secret scanners and violate security best practices. Configure via environment variables only and use a conditional wrapper to skip dependent test suites when the key is unavailable.
🔐 Safer configuration
-const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE';
+const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
+const describeIfSupabase = SUPABASE_ANON_KEY ? describe : describe.skip;
@@
-describe('Supabase Integration', () => {
+describeIfSupabase('Supabase Integration', () => {Apply the same describeIfSupabase wrapper to the 'RLS Policy Validation' suite (line 228), which also depends on the anon key. The 'Chat Service Integration' suite (line 202) uses only mock data and does not require this change.
🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 16-16: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.
(jwt)
🤖 Prompt for AI Agents
In `@tests/unit/supabase.test.ts` around lines 15 - 20, Remove the hardcoded demo
JWT by eliminating the fallback value for SUPABASE_ANON_KEY so it only reads
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, add a conditional test wrapper (e.g.,
describeIfSupabase) that checks presence of SUPABASE_ANON_KEY and calls describe
or describe.skip accordingly, and replace the top-level describe usage for the
"Schema Validation" suite with describeIfSupabase(SUPABASE_ANON_KEY, 'Schema
Validation', ...) to skip when the key is missing; also apply the same
describeIfSupabase wrapper to the "RLS Policy Validation" suite (leave the "Chat
Service Integration" suite unchanged since it uses mocks).
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Database & Cloud
Observability
Documentation
Utilities & Testing