Skip to content

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Oct 2, 2025

User description

This commit introduces a new asynchronous agent, geojsonEnricher, responsible for processing textual responses and extracting structured geospatial data.

Key changes:

  • Created a new geojsonEnricher agent in lib/agents/geojson-enricher.tsx.
  • Defined new TypeScript interfaces for LocationResponse, MapCommand, and GeoJSON objects in lib/types/custom.ts.
  • Integrated the new agent into the main application workflow in app/actions.tsx.
  • Added a LocationResponseHandler component to update the map state via the MapDataContext.
  • Updated the Mapbox component to render GeoJSON data and execute map commands.

PR Type

Enhancement


Description

  • Add GeoJSON enrichment agent for geospatial data extraction

  • Integrate map visualization with GeoJSON rendering capabilities

  • Create location response handler for map state management

  • Define TypeScript interfaces for geospatial data structures


Diagram Walkthrough

flowchart LR
  A["Text Response"] --> B["GeoJSON Enricher Agent"]
  B --> C["LocationResponse"]
  C --> D["LocationResponseHandler"]
  D --> E["MapDataContext"]
  E --> F["Mapbox Component"]
  F --> G["Rendered Map with GeoJSON"]
Loading

File Walkthrough

Relevant files
Enhancement
custom.ts
Define geospatial TypeScript interfaces                                   

lib/types/custom.ts

  • Define TypeScript interfaces for geospatial data structures
  • Add LocationResponse, MapCommand, and GeoJSON types
  • Support Point, LineString, and Polygon geometries
+33/-0   
actions.tsx
Integrate GeoJSON enrichment into main workflow                   

app/actions.tsx

  • Import and integrate geojsonEnricher agent
  • Add LocationResponseHandler component integration
  • Process location responses in submit workflow
  • Handle geojsonEnrichment tool output in UI state
+39/-1   
mapbox-map.tsx
Add GeoJSON rendering and map command execution                   

components/map/mapbox-map.tsx

  • Add GeoJSON data rendering with points and lines layers
  • Implement map command execution (flyTo, easeTo, fitBounds)
  • Handle dynamic source and layer management
  • Clear commands after execution to prevent re-triggering
+78/-0   
geojson-enricher.tsx
Create GeoJSON enrichment agent                                                   

lib/agents/geojson-enricher.tsx

  • Create new agent for extracting geospatial data from text
  • Implement specialized prompt for GeoJSON and map command extraction
  • Handle JSON parsing with error fallback
  • Return structured LocationResponse objects
+66/-0   
map-data-context.tsx
Extend map context with geospatial data                                   

components/map/map-data-context.tsx

  • Add geojson and mapCommands fields to MapData interface
  • Import required types from custom types module
+6/-0     
location-response-handler.tsx
Create location response handler component                             

components/map/location-response-handler.tsx

  • Create component to handle location response updates
  • Update map context with GeoJSON and map commands
  • Implement side-effect only component (no UI rendering)
+28/-0   
Dependencies
package.json
Update dependencies for geospatial features                           

package.json

  • Add @radix-ui/react-select, nanoid, and zod dependencies
  • Update existing package versions
  • Remove QCX dependency reference
+8/-6     

Summary by CodeRabbit

  • New Features

    • Rich location responses with GeoJSON overlays, generated camera actions (fly/zoom/pan), live map-state feedback UI, map status indicator, and optional Mapbox MCP integration for enhanced geocoding.
  • Bug Fixes

    • Safer fallbacks and stricter validation with improved error handling for enrichment, map commands, and UI rendering.
  • Performance

    • Streamlined enrichment flow for faster, more reliable results; increased build memory for larger builds.
  • Documentation & Tests

    • New implementation and debugging guides plus test scripts for enricher and map agents.

This commit introduces a new asynchronous agent, `geojsonEnricher`, responsible for processing textual responses and extracting structured geospatial data.

Key changes:
- Created a new `geojsonEnricher` agent in `lib/agents/geojson-enricher.tsx`.
- Defined new TypeScript interfaces for `LocationResponse`, `MapCommand`, and GeoJSON objects in `lib/types/custom.ts`.
- Integrated the new agent into the main application workflow in `app/actions.tsx`.
- Added a `LocationResponseHandler` component to update the map state via the `MapDataContext`.
- Updated the `Mapbox` component to render GeoJSON data and execute map commands.
@charliecreates charliecreates bot requested a review from CharlieHelps October 2, 2025 09:55
@vercel
Copy link
Contributor

vercel bot commented Oct 2, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
qcx Ready Ready Preview, Comment Feb 7, 2026 6:00pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Walkthrough

Adds a multi-agent Map Control Orchestrator, worker modules, and a Composio Mapbox integration; introduces Zod schemas for map data, geojson enrichment (v1/v2), map command generation, map feedback capture/emit, map UI wiring (LocationResponseHandler, MapStatus, mapbox command execution), tests, and documentation.

Changes

Cohort / File(s) Summary
Types & Schemas
lib/types/map-schemas.ts, lib/types/custom.ts
New strict Zod schemas and TS types for GeoJSON, MapCommand, MapStateFeedback, FeedbackAnalysis, LocationResponse; re-exported types in custom.ts.
Orchestrator & LangGraph
lib/agents/map-control-orchestrator.tsx, lib/agents/map-langgraph.tsx, lib/agents/index.tsx
New MapControlOrchestrator class, helper mapControlOrchestrator, and a LangGraph map workflow (runMapGraph) implementing classification, MCP/Composio interactions, iterative feedback, and orchestration state.
Workers
lib/agents/map-workers/geojson-parser.tsx, .../map-command-generator.tsx, .../validator.tsx, .../feedback-analyzer.tsx
New worker modules for GeoJSON parsing, map command generation, semantic validation, and feedback analysis — LLM-first with structured schema validation and deterministic fallbacks.
GeoJSON Enrichment
lib/agents/geojson-enricher.tsx, lib/agents/geojson-enricher-v2.tsx
Added original enricher and a robust single-call geojsonEnricherV2 (with geojsonEnricher wrapper) that parses LLM output into LocationResponse, with optional MCP/Composio client support and fallbacks.
Geospatial Tooling / Composio
lib/agents/tools/geospatial.tsx
Replaced legacy MCP SDK wiring with initializeComposioMapbox flow; simplified action mapping to Composio actions; removed some legacy exported type aliases and adjusted getConnectedMcpClient behavior.
Map UI & Context
components/map/location-response-handler.tsx, components/map/mapbox-map.tsx, components/map/map-data-context.tsx, components/map/map-status.tsx
New LocationResponseHandler side‑effect component merges LocationResponse into map data; mapbox component now manages geojson sources/layers, executes camera commands (flyTo/easeTo/fitBounds/setCenter/setZoom/setPitch/setBearing), captures/emits MapStateFeedback with moveend+timeout fallback, and supports feedbackCallback; MapData extended with geojson, mapCommands, and feedback fields; MapStatus UI added.
App Integration & Actions
app/actions.tsx
Wiring added to initialize Composio/Composio-aware flows, attempt orchestration (LangGraph/MapControlOrchestrator), update UI MapStatus, and fall back to geojsonEnricherV2 or minimal responses on failure; tightened types to PartialRelated/PartialInquiry.
Tests & Scripts
test-map-agents.ts, test-enricher.ts, package.json
New test scripts for parser/generator/validator/orchestrator/enricher; package.json adds langchain/langgraph deps, @radix-ui/react-select, nanoid, bumps TS/typings and increases Node build memory.
Docs & Guides
IMPLEMENTATION_GUIDE.md, SOLUTION_SUMMARY.md, lib/agents/MAP_AGENTS_README.md, docs/*, DEBUGGING_MAP_INTEGRATION.md, DEBUGGING_FIXES_SUMMARY.md
Extensive design, implementation, migration, debugging, testing, and rollout documentation for the orchestrator/worker architecture and Composio integration.
Misc / Types re-exports
lib/types/custom.ts, lib/types/custom.ts
Re-exported map-related public types for backward compatibility.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as app/actions.tsx
    participant Orch as MapControlOrchestrator / runMapGraph
    participant Parser as GeoJSON Parser
    participant Generator as Map Command Generator
    participant Validator as Validator
    participant Map as Mapbox Map (mapbox-map.tsx)
    participant Analyzer as Feedback Analyzer

    User->>App: submit researcher response
    App->>Orch: process(researcherResponse) / runMapGraph(...)
    Orch->>Parser: geojsonParser(text)
    Parser-->>Orch: geojson + confidence
    Orch->>Generator: mapCommandGenerator(context)
    Generator-->>Orch: map_commands
    Orch->>Validator: validateGeoJSON / validateMapCommands
    Validator-->>Orch: ValidationResult
    Orch-->>App: LocationResponse (text, geojson, map_commands)
    App->>Map: setMapData(geojson, mapCommands)
    Map->>Map: execute commands (flyTo/fitBounds/etc.)
    Map-->>App: feedbackCallback(MapStateFeedback)
    alt feedback loop enabled
        App->>Orch: processFeedback(feedback)
        Orch->>Analyzer: feedbackAnalyzer(feedback + context)
        Analyzer-->>Orch: FeedbackAnalysis
        Orch-->>App: refined LocationResponse or abort
    end
    App->>User: emit geojsonEnrichment tool output / update UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Suggested reviewers

  • CharlieHelps

Poem

🐇
I hopped through prompts and schemas bright,
Features and commands took flight at night,
Maps hum and spin with a joyful ping,
Feedback loops teach the next small thing,
Hooray — this rabbit charts the light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Create GeoJSON Enrichment Agent' accurately describes the main change: introducing a new geojsonEnricher agent and LocationResponse type for extracting geospatial data from text.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/geojson-enrichment-agent

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant
Copy link

CLAassistant commented Oct 2, 2025

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 3 committers have signed the CLA.

✅ ngoiyaeric
❌ google-labs-jules[bot]
❌ CJWTRUST
You have signed the CLA already but the status is still pending? Let us recheck it.

@charliecreates
Copy link

charliecreates bot commented Oct 2, 2025

  1. The introduction of the geojsonEnricher agent is a positive feature for enhancing geospatial data extraction. The design is clear, and the integration touches all the necessary components.
  2. The new TypeScript types in lib/types/custom.ts make the structure of geospatial data explicit. It’s good to see strong typing here. Double-check that those interfaces cover all possible variants of GeoJSON you expect to encounter (e.g., properties, complex FeatureCollections).
  3. The LocationResponseHandler component is a welcome addition for updating map state, but it would be helpful to see additional error handling/logging in case of unexpected data.
  4. The update to Mapbox to render GeoJSON and handle map commands appears well-structured, but please ensure that the rendering logic handles edge cases, like empty or invalid features.
  5. App-level integration (app/actions.tsx) looks appropriate but consider isolating complex logic into smaller, testable functions for maintainability.
  6. There are multiple lockfile updates (bun.lockb, pnpm-lock.yaml). Be sure only the correct lockfile is present and committed to avoid install issues.
  7. Consider adding unit or integration tests for the new agent and its TypeScript types, especially for edge geospatial scenarios.

Overall, great progress expanding the application’s geospatial functionality. With a couple more tests and error-handling tweaks, this will be even more robust.

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Oct 2, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Unvalidated LLM output

Description: Unvalidated JSON from an LLM is parsed and then propagated to the app, enabling potential
prototype pollution or unexpected map command execution without schema validation or
command allowlisting.
geojson-enricher.tsx [48-56]

Referred Code
const { text } = await streamText({
  model,
  messages,
  maxTokens: 2048,
});

// Assuming the LLM returns a valid JSON string, parse it.
const enrichedData = JSON.parse(text) as LocationResponse;
return enrichedData;
Unvalidated map commands

Description: Arbitrary map commands from external data are executed (flyTo, easeTo, fitBounds) without
validation or bounds checks, allowing abusive parameter values that can cause DoS-like
behavior or reveal internal state.
mapbox-map.tsx [580-599]

Referred Code
if (!map.current || !mapData.mapCommands || mapData.mapCommands.length === 0) return;

const mapInstance = map.current;

mapData.mapCommands.forEach(command => {
  switch (command.command) {
    case 'flyTo':
      mapInstance.flyTo(command.params);
      break;
    case 'easeTo':
      mapInstance.easeTo(command.params);
      break;
    case 'fitBounds':
      mapInstance.fitBounds(command.params, command.params.options || {});
      break;
    default:
      console.warn(`Unknown map command: ${command.command}`);
  }
});
Unvalidated GeoJSON input

Description: GeoJSON data from untrusted LLM output is injected into the map without schema validation,
risking large or malformed payloads that could trigger performance issues or parser
vulnerabilities.
mapbox-map.tsx [529-575]

Referred Code
if (!map.current) return;

const mapInstance = map.current;
const source = mapInstance.getSource('geojson-data');

// If GeoJSON data is present, add or update the source and layers
if (mapData.geojson) {
  if (source) {
    (source as mapboxgl.GeoJSONSource).setData(mapData.geojson);
  } else {
    mapInstance.addSource('geojson-data', {
      type: 'geojson',
      data: mapData.geojson,
    });

    // Add layer for points
    mapInstance.addLayer({
      id: 'geojson-points',
      type: 'circle',
      source: 'geojson-data',
      paint: {


 ... (clipped 26 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
No custom compliance provided

Follow the guide to enable custom compliance check.

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Oct 2, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix incorrect fitBounds method usage
Suggestion Impact:The commit updated the fitBounds call to destructure bounds and options from command.params and pass them separately, aligning with the suggested fix (without the extra validation/logging).

code diff:

@@ -590,7 +590,8 @@
           mapInstance.easeTo(command.params);
           break;
         case 'fitBounds':
-          mapInstance.fitBounds(command.params, command.params.options || {});
+          const { bounds, options } = command.params;
+          mapInstance.fitBounds(bounds, options || {});
           break;

Correct the fitBounds method call by passing command.params.bounds and
command.params.options as separate arguments, as the current implementation
passes the entire params object incorrectly.

components/map/mapbox-map.tsx [592-594]

 case 'fitBounds':
-  mapInstance.fitBounds(command.params, command.params.options || {});
+  // The 'params' object for fitBounds should have a 'bounds' property.
+  if ('bounds' in command.params) {
+    mapInstance.fitBounds(command.params.bounds, command.params.options || {});
+  } else {
+    console.warn(`Invalid 'fitBounds' command: 'bounds' property is missing in params.`);
+  }
   break;

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a runtime error where fitBounds is called with incorrect arguments, and it provides a correct implementation that aligns with the Mapbox GL JS API.

High
Improve robustness of JSON parsing
Suggestion Impact:The commit added logic to strip markdown code fences from the LLM response before calling JSON.parse, improving robustness similarly to the suggestion (though using regex for code fences rather than scanning for braces).

code diff:

     // Assuming the LLM returns a valid JSON string, parse it.
-    const enrichedData = JSON.parse(text) as LocationResponse;
+    let responseText = await text;
+
+    // Strip markdown code fences if present
+    const jsonMatch = responseText.match(/```(json)?\n([\s\S]*?)\n```/);
+    if (jsonMatch && jsonMatch[2]) {
+      responseText = jsonMatch[2].trim();
+    }
+
+    const enrichedData = JSON.parse(responseText) as LocationResponse;

To improve parsing robustness, extract the JSON object from the raw LLM response
text before parsing, as the response may contain extra text or markdown.

lib/agents/geojson-enricher.tsx [54-56]

-// Assuming the LLM returns a valid JSON string, parse it.
-const enrichedData = JSON.parse(text) as LocationResponse;
+// Extract the JSON object from the text response.
+const jsonStart = text.indexOf('{');
+const jsonEnd = text.lastIndexOf('}');
+if (jsonStart === -1 || jsonEnd === -1) {
+  throw new Error('No JSON object found in the LLM response.');
+}
+const jsonString = text.substring(jsonStart, jsonEnd + 1);
+
+// Parse the extracted JSON string.
+const enrichedData = JSON.parse(jsonString) as LocationResponse;
 return enrichedData;

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that direct JSON.parse on LLM output is fragile and proposes a robust method to extract the JSON string, significantly improving the reliability of the feature.

Medium
Improve GeoJSON type safety
Suggestion Impact:The commit removed the local GeoJSONGeometry interface (with broad union-typed coordinates) and instead re-exported GeoJSONGeometry from a new './map-schemas' module for backward compatibility, indicating the GeoJSON types were centralized elsewhere (where the improved/discriminated typing could be implemented).

code diff:

+// Re-export types from map-schemas for backward compatibility
+export type {
+  MapCommand,
+  GeoJSONGeometry,
+  GeoJSONFeature,
+  GeoJSONFeatureCollection,
+  LocationResponse,
+} from './map-schemas';

Refactor the GeoJSONGeometry interface into a discriminated union to enforce
that each geometry type has the correct corresponding coordinates structure,
improving type safety.

lib/types/custom.ts [7-11]

-// Defines the structure for the geometry part of a GeoJSON feature.
-export interface GeoJSONGeometry {
-  type: 'Point' | 'LineString' | 'Polygon'; // Can be extended with other GeoJSON geometry types
-  coordinates: number[] | number[][] | number[][][];
-}
+// Defines the structure for specific GeoJSON geometry types.
+type PointGeometry = {
+  type: 'Point';
+  coordinates: number[];
+};
 
+type LineStringGeometry = {
+  type: 'LineString';
+  coordinates: number[][];
+};
+
+type PolygonGeometry = {
+  type: 'Polygon';
+  coordinates: number[][][];
+};
+
+// Defines the structure for the geometry part of a GeoJSON feature using a discriminated union.
+export type GeoJSONGeometry = PointGeometry | LineStringGeometry | PolygonGeometry;
+

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a type safety weakness and proposes a discriminated union, which is the idiomatic and safer way to model this data structure in TypeScript, improving code quality and robustness.

Low
General
Prevent unnecessary component re-renders

To prevent unnecessary re-renders, add a deep comparison check inside the
useEffect hook to ensure setMapData is only called when the locationResponse
data has actually changed.

components/map/location-response-handler.tsx [14-24]

+// Custom hook to get the previous value of a prop or state
+function usePrevious<T>(value: T): T | undefined {
+  const ref = useRef<T>();
+  useEffect(() => {
+    ref.current = value;
+  });
+  return ref.current;
+}
+
+// ... inside LocationResponseHandler component
+const { setMapData } = useMapData();
+const prevLocationResponse = usePrevious(locationResponse);
+
 useEffect(() => {
-  if (locationResponse) {
+  // Prevent unnecessary updates if the response has not changed.
+  // A deep equality check is needed for objects.
+  if (locationResponse && JSON.stringify(locationResponse) !== JSON.stringify(prevLocationResponse)) {
     const { geojson, map_commands } = locationResponse;
-    console.log('LocationResponseHandler: Received data', locationResponse);
+    console.log('LocationResponseHandler: Received new data', locationResponse);
     setMapData(prevData => ({
       ...prevData,
       geojson: geojson,
       mapCommands: map_commands,
     }));
   }
-}, [locationResponse, setMapData]);
+}, [locationResponse, prevLocationResponse, setMapData]);

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why: The suggestion offers a valid performance optimization to prevent unnecessary re-renders, but its impact is moderate as it addresses a potential, not a definite, performance issue.

Low
  • Update

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b6d0e1 and dea29ee.

⛔ Files ignored due to path filters (2)
  • bun.lockb is excluded by !**/bun.lockb
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • app/actions.tsx (6 hunks)
  • components/map/location-response-handler.tsx (1 hunks)
  • components/map/map-data-context.tsx (2 hunks)
  • components/map/mapbox-map.tsx (1 hunks)
  • lib/agents/geojson-enricher.tsx (1 hunks)
  • lib/types/custom.ts (1 hunks)
  • package.json (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
lib/agents/geojson-enricher.tsx (2)
lib/types/custom.ts (1)
  • LocationResponse (29-33)
lib/utils/index.ts (1)
  • getModel (19-62)
components/map/location-response-handler.tsx (4)
lib/types/custom.ts (1)
  • LocationResponse (29-33)
components/map/map-data-context.tsx (1)
  • useMapData (42-48)
components/map/map-query-handler.tsx (4)
  • useMapData (30-83)
  • toolOutput (33-74)
  • McpResponseData (8-16)
  • prevData (39-49)
lib/agents/tools/geospatial.tsx (1)
  • McpResponse (22-25)
components/map/map-data-context.tsx (1)
lib/types/custom.ts (2)
  • GeoJSONFeatureCollection (23-26)
  • MapCommand (2-5)
app/actions.tsx (5)
lib/agents/geojson-enricher.tsx (1)
  • geojsonEnricher (36-66)
components/map/location-response-handler.tsx (1)
  • LocationResponseHandler (11-28)
lib/agents/tools/geospatial.tsx (3)
  • uiStream (144-269)
  • McpResponse (22-25)
  • Location (15-20)
components/map/map-query-handler.tsx (3)
  • toolOutput (33-74)
  • useMapData (30-83)
  • prevData (39-49)
lib/agents/tools/index.tsx (1)
  • ToolProps (14-39)
🔇 Additional comments (5)
lib/agents/geojson-enricher.tsx (3)

1-3: LGTM!

Imports are correctly typed and used appropriately throughout the file.


39-45: LGTM!

Model initialization and message construction are correct. The type assertion is safe given getModel() always returns a LanguageModel-compatible object.


57-65: LGTM!

Error handling is appropriate: the function logs the error and returns a safe fallback with the original text, ensuring graceful degradation if enrichment fails.

app/actions.tsx (2)

15-15: LGTM!

Import statement is correct and the function is properly used at line 301.


29-29: LGTM!

Import statement is correct and the component is used at lines 556 and 591.

@ngoiyaeric
Copy link
Collaborator Author

@coderabbitai generate unit tests

@ngoiyaeric
Copy link
Collaborator Author

@coderabbitai the code is not building

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Checking package.json for syntax errors ==="
cat package.json | jq '.' > /dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "✓ package.json is valid JSON"
else
    echo "✗ package.json has syntax errors"
fi

echo -e "\n=== Checking TypeScript files for syntax issues ==="

echo -e "\n--- Checking lib/types/custom.ts ---"
cat lib/types/custom.ts

echo -e "\n--- Checking lib/agents/geojson-enricher.tsx ---"
cat lib/agents/geojson-enricher.tsx

echo -e "\n--- Checking components/map/location-response-handler.tsx ---"
cat components/map/location-response-handler.tsx

echo -e "\n--- Checking components/map/map-data-context.tsx ---"
cat components/map/map-data-context.tsx

echo -e "\n--- Checking app/actions.tsx for import issues ---"
head -50 app/actions.tsx

echo -e "\n=== Checking for missing dependencies or imports ==="
echo "Checking if getModel is imported/exported..."
rg -n "export.*getModel" --type=ts --type=tsx

echo -e "\nChecking if LocationResponse type is properly exported..."
rg -n "export.*LocationResponse" lib/types/custom.ts

echo -e "\n=== Checking tsconfig.json ==="
if [ -f tsconfig.json ]; then
    cat tsconfig.json
else
    echo "tsconfig.json not found"
fi

Length of output: 9590


@ngoiyaeric, I found the build issue! There's a syntax error in lib/agents/geojson-enricher.tsx on Line 2.

The Problem

The import statement is missing a space between } and from:

import { getModel }from '../utils';
//                  ↑ missing space here

The Fix

Change Line 2 in lib/agents/geojson-enricher.tsx from:

import { getModel }from '../utils';

to:

import { getModel } from '../utils';

This is a simple typo that will prevent TypeScript from compiling. Once you add the space, the build should succeed.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Unit Test Generation failed: CodeRabbit timed out while planning changes for components/map/map-data-context.test.tsx. Skipping this file.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

✅ Actions performed

Unit Test Generation started...

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Unit Test Generation failed: CodeRabbit timed out while planning changes for components/map/location-response-handler.test.tsx. Skipping this file.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

✅ Actions performed

Unit Test Generation started...

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Unit Test Generation failed: CodeRabbit timed out while planning changes for app/actions.test.tsx. Skipping this file.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

✅ Actions performed

Unit Test Generation started...

This commit introduces a new asynchronous agent, `geojsonEnricher`, responsible for processing textual responses and extracting structured geospatial data.

Key changes:
- Created a new `geojsonEnricher` agent in `lib/agents/geojson-enricher.tsx`.
- Defined new TypeScript interfaces for `LocationResponse`, `MapCommand`, and GeoJSON objects in `lib/types/custom.ts`.
- Integrated the new agent into the main application workflow in `app/actions.tsx` with robust error handling.
- Added a `LocationResponseHandler` component to update the map state via the `MapDataContext`.
- Updated the `Mapbox` component to render GeoJSON data and execute map commands.

NOTE: The application build is currently failing due to a persistent 'JavaScript heap out of memory' error. This appears to be a pre-existing issue with the repository's dependencies or build configuration that could not be resolved. The feature code is complete, but the build itself will need to be addressed separately.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/actions.tsx (1)

539-548: Wrap related case in a block to satisfy no-switch-declarations.

Declaring const relatedQueries = … directly under the case triggers Biome’s noSwitchDeclarations error because the binding leaks into other branches. Enclose the case 'related' body in braces (or extract to a helper) so the declaration stays scoped to that branch.

♻️ Duplicate comments (1)
components/map/mapbox-map.tsx (1)

585-595: Guard fitBounds params before destructuring.

command.params can be undefined when enrichment returns a malformed or partial command. Destructuring it directly throws and prevents later commands from running. Add a clause block, validate bounds, and bail early if missing.

-        case 'fitBounds':
-          const { bounds, options } = command.params;
-          mapInstance.fitBounds(bounds, options || {});
-          break;
+        case 'fitBounds': {
+          const params = command.params ?? {};
+          const { bounds, options } = params as {
+            bounds?: mapboxgl.FitBoundsLike;
+            options?: mapboxgl.FitBoundsOptions;
+          };
+          if (!bounds) {
+            console.warn('fitBounds command missing bounds payload', command);
+            break;
+          }
+          mapInstance.fitBounds(bounds, options ?? {});
+          break;
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dea29ee and a5fa68f.

⛔ Files ignored due to path filters (1)
  • bun.lockb is excluded by !**/bun.lockb
📒 Files selected for processing (4)
  • app/actions.tsx (7 hunks)
  • components/map/mapbox-map.tsx (1 hunks)
  • lib/agents/geojson-enricher.tsx (1 hunks)
  • package.json (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
components/map/mapbox-map.tsx (2)
components/map/map-data-context.tsx (1)
  • MapData (7-17)
components/map/map-query-handler.tsx (4)
  • prevData (39-49)
  • prevData (53-57)
  • toolOutput (33-74)
  • useMapData (30-83)
lib/agents/geojson-enricher.tsx (2)
lib/types/custom.ts (1)
  • LocationResponse (29-33)
lib/utils/index.ts (1)
  • getModel (19-62)
app/actions.tsx (3)
lib/agents/geojson-enricher.tsx (1)
  • geojsonEnricher (37-75)
lib/schema/related.tsx (1)
  • PartialRelated (13-13)
components/map/location-response-handler.tsx (1)
  • LocationResponseHandler (11-28)
🪛 Biome (2.1.2)
components/map/mapbox-map.tsx

[error] 593-593: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

app/actions.tsx

[error] 539-539: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

@ngoiyaeric
Copy link
Collaborator Author

@coderabbitai review pr

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 13, 2025

@ngoiyaeric, I'll review the PR changes for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ngoiyaeric ngoiyaeric added Map Backend Issues on schema and database Agent labels Oct 13, 2025
Copy link
Collaborator Author

@ngoiyaeric ngoiyaeric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be a zod schema for the GeoJson parsing to improve its precision.

@ngoiyaeric ngoiyaeric added the 1.0.0 Standard release label Oct 24, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/map/mapbox-map.tsx (1)

772-821: Duplicate useEffect with identical cleanup logic.

This useEffect block duplicates the cleanup logic from the earlier initialization effect (lines 491-521). The effect body contains only placeholder comments (// ... existing useEffect logic ...) but includes a full cleanup function, creating duplicate cleanup handlers.

This will cause:

  1. Double cleanup execution when dependencies change
  2. Errors when attempting to remove already-removed controls
  3. Potential race conditions between the two cleanup functions
🐛 Recommended fix: Remove the duplicate effect

Remove lines 772-821 entirely, as the cleanup is already handled by the initialization effect at lines 491-521.

-  // Cleanup for the main useEffect
-  useEffect(() => {
-    // ... existing useEffect logic ...
-    return () => {
-      // ... existing cleanup logic ...
-      if (longPressTimerRef.current) { // Cleanup timer on component unmount
-        clearTimeout(longPressTimerRef.current);
-        longPressTimerRef.current = null;
-      }
-      // ... existing cleanup logic for map and geolocation ...
-      if (map.current) {
-        map.current.off('moveend', captureMapCenter)
-        // ... rest of cleanup ...
-      }
-      // ...
-    };
-  }, [
-    handleUserInteraction,
-    startRotation,
-    stopRotation,
-    mapType,
-    updateMeasurementLabels,
-    setupGeolocationWatcher,
-    captureMapCenter,
-    setupDrawingTools,
-    setIsMapLoaded,
-    setMap
-  ]);

Instead, add the long-press timer cleanup to the existing effect at line 491:

  return () => {
+   if (longPressTimerRef.current) {
+     clearTimeout(longPressTimerRef.current);
+     longPressTimerRef.current = null;
+   }
    if (map.current) {
      map.current.off('moveend', captureMapCenter)
      // ... rest of existing cleanup
🤖 Fix all issues with AI agents
In @components/map/mapbox-map.tsx:
- Around line 677-685: Replace the use of any for the feedback object by
defining a proper interface (e.g., MapFeedback) that matches the shape used in
this block (success: boolean, currentCenter: [number, number], currentZoom:
number, currentPitch: number, currentBearing: number, error: Error | null |
string, timestamp: number) and annotate the feedback variable as that interface
(const feedback: MapFeedback = {...}); ensure the types of center,
executionError and the return types from mapInstance.getZoom/getPitch/getBearing
are compatible or cast appropriately so the assignment is type-safe.
- Around line 672-720: The feedbackHandler can run twice due to both
mapInstance.once('moveend') and the fallback setTimeout; add a local guard flag
(e.g., handled = false) captured by the closure and check/set it at the start of
feedbackHandler to ensure idempotence, cancel the timeout when moveend fires
(store the timeout id from setTimeout and clearTimeout in the moveend path), and
extend/remove the short 100ms fallback for long animations (or change it to a
longer configurable timeout); update references in this block (feedbackHandler,
mapInstance.once('moveend'), mapInstance.off('moveend'), the setTimeout call,
and the setMapData/mapData.feedbackCallback usage) to use the guard and timeout
cancellation so feedback is only sent once.
- Around line 610-629: The useEffect in the MapboxMap component currently emits
several console.log calls (e.g., the initial `'🗺️  Map commands useEffect
triggered:'` log, the `'⚠️ Skipping command execution'` log, and per-command
execution logs inside the loop referencing mapData.mapCommands and map.current);
remove these console.logs or gate them behind a configurable debug flag (e.g., a
local isDebug prop/env check such as process.env.NODE_ENV !== 'production' or a
passed debugLogs boolean) and replace with a proper logger if needed so
production builds do not output or incur the cost of these messages; ensure any
conditional checks use that flag before logging and keep the control flow and
error handling (executionError, try/catch) unchanged.
- Around line 566-572: The current code casts mapData.geojson to any when
calling (source as mapboxgl.GeoJSONSource).setData(...) and when passing data to
mapInstance.addSource, which loses type safety; instead, type mapData.geojson
with proper GeoJSON types (e.g., GeoJSON.FeatureCollection or GeoJSON.GeoJSON)
or Mapbox's mapboxgl.GeoJSONSourceRaw, import the GeoJSON types from 'geojson'
or use the types from 'mapbox-gl', and pass that typed value to setData and
addSource without using any; update the mapData type definition and replace the
any casts in the calls to use the correct typed object so setData and addSource
receive a properly typed GeoJSON value.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93c47a9 and a74b7e8.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • components/map/mapbox-map.tsx
🔇 Additional comments (1)
components/map/mapbox-map.tsx (1)

574-598: No Polygon layer for GeoJSON rendering.

The code adds layers for Point and LineString features but not for Polygon. If the GeoJSON enrichment produces polygon geometries, they will not be visible on the map.

Consider adding a polygon fill/outline layer if needed:

+       // Add layer for polygons
+       mapInstance.addLayer({
+         id: 'geojson-polygons',
+         type: 'fill',
+         source: 'geojson-data',
+         paint: {
+           'fill-color': '#007cbf',
+           'fill-opacity': 0.3,
+         },
+         filter: ['==', '$type', 'Polygon'],
+       });

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/map/mapbox-map.tsx (1)

795-841: Duplicate cleanup effect may cause double cleanup attempts.

This cleanup effect (lines 795-841) has nearly identical logic and the same dependency array as the main initialization effect's cleanup (lines 498-528). When the component unmounts or dependencies change, both cleanups will run, potentially causing errors when trying to remove already-removed resources.

Consider consolidating cleanup logic into a single effect or ensuring guards prevent double-cleanup.

app/actions.tsx (1)

629-639: Switch case declaration should be wrapped in a block.

As flagged by Biome, declaring relatedQueries directly in the switch case allows other cases to potentially access it. Wrap the case body in a block.

Proposed fix
-            case 'related':
-              const relatedQueries = createStreamableValue<PartialRelated>()
-              relatedQueries.done(JSON.parse(content as string))
-              return {
-                id,
-                component: (
-                  <Section title="Related" separator={true}>
-                    <SearchRelated relatedQueries={relatedQueries.value} />
-                  </Section>
-                )
-              }
+            case 'related': {
+              const relatedQueries = createStreamableValue<PartialRelated>()
+              relatedQueries.done(JSON.parse(content as string))
+              return {
+                id,
+                component: (
+                  <Section title="Related" separator={true}>
+                    <SearchRelated relatedQueries={relatedQueries.value} />
+                  </Section>
+                )
+              }
+            }
🤖 Fix all issues with AI agents
In @app/actions.tsx:
- Around line 311-312: Replace the unsafe cast "(inquiry as any)?.question" by
giving inquiry a proper type: update the inquire function's return type (or the
variable declaration for inquiry) to an interface/type that includes the
question property (e.g., type Inquiry = { question: string; ... }) and use that
type instead of any; alternatively add a narrow type guard function
isInquiry(x): x is Inquiry and use if (isInquiry(inquiry)) inquiry.question.
Ensure usages reference the inquire function and the inquiry variable so
TypeScript enforces the presence of question rather than using "any".
- Around line 16-17: The file contains unused imports mapControlOrchestrator and
initializeComposioMapbox; remove these two import statements from
app/actions.tsx to eliminate dead code and unused dependencies, ensuring no
other references to mapControlOrchestrator or initializeComposioMapbox remain in
that file and run a quick build/typecheck to confirm there are no missing
usages.

In @components/map/mapbox-map.tsx:
- Around line 677-739: The effect can leak because fallbackTimeoutId and the
moveend listener referencing feedbackHandler aren't cleaned up on
re-run/unmount; declare feedbackHandler and fallbackTimeoutId in stable scope
(or store them in refs) so the cleanup can access the same references, and in
the effect return clearTimeout(fallbackTimeoutId) and remove the listener via
mapInstance.off('moveend', feedbackHandler) (also set handled = true or similar
guard) to ensure the timeout and event listener are always removed; adjust
placement so feedbackHandler is defined outside try/async blocks or use useRef
to hold the handler before registering mapInstance.once.
- Around line 642-651: The fitBoundsOptions variable is currently typed as any
which loses type safety; change its declaration to use mapboxgl.FitBoundsOptions
(e.g., initialize as {} as mapboxgl.FitBoundsOptions) and keep assigning
padding/duration/otherOptions into it before calling
mapInstance.fitBounds(bounds, fitBoundsOptions); ensure mapboxgl is available in
scope (import or reference the global type) so the compiler recognizes
mapboxgl.FitBoundsOptions and you get IDE/type checking for the fitBounds call.
- Around line 559-609: The effect that updates the 'geojson-data' source must
guard against style not being loaded and also add polygon support: check
map.current and use mapInstance.isStyleLoaded() (or listen for the 'styledata'
event) before calling addSource/addLayer to avoid race errors, and if waiting
for style, retry or attach a one-time listener; when adding layers, add two
polygon layers (e.g., 'geojson-polygons-fill' with type 'fill' and appropriate
'fill-color'/'fill-opacity' and 'geojson-polygons-outline' with type 'line' for
stroke) and use filters for 'Polygon' and 'MultiPolygon'; also ensure you remove
those polygon layers when mapData.geojson is falsy and when updating source,
mirror existing logic for points and lines using the same source id
'geojson-data'.
- Around line 19-28: Replace the local MapFeedback interface with the
centralized MapStateFeedback type: remove the MapFeedback interface declaration
in mapbox-map.tsx, add an import for MapStateFeedback from
"lib/types/map-schemas", and update any usages or annotations that reference
MapFeedback to use MapStateFeedback instead (no code logic changes needed since
all fields are provided where used).
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a74b7e8 and 4f989c2.

📒 Files selected for processing (2)
  • app/actions.tsx
  • components/map/mapbox-map.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
components/map/mapbox-map.tsx (4)
components/map-toggle-context.tsx (1)
  • useMapToggle (36-42)
components/map/map-data-context.tsx (1)
  • useMapData (55-61)
components/map-loading-context.tsx (1)
  • useMapLoading (20-26)
components/map/geojson-layer.tsx (4)
  • map (16-97)
  • map (89-96)
  • map (24-80)
  • GeoJsonLayer (13-100)
🪛 Biome (2.1.2)
app/actions.tsx

[error] 630-631: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (3)
app/actions.tsx (3)

688-708: LGTM with suggestion: Consider validating map_commands field.

The runtime validation for geojsonEnrichment is good defensive programming. However, consider also validating the map_commands field (should be null or an array) to ensure complete validation of the LocationResponse structure.

Optional enhancement
             if (
               toolOutput &&
               typeof toolOutput.text === 'string' &&
               (toolOutput.geojson === null ||
                 (typeof toolOutput.geojson === 'object' &&
-                  toolOutput.geojson.type === 'FeatureCollection'))
+                  toolOutput.geojson.type === 'FeatureCollection')) &&
+              (toolOutput.map_commands === null ||
+                Array.isArray(toolOutput.map_commands))
             ) {

420-426: LGTM!

The tool message structure for geojsonEnrichment follows the existing pattern and correctly stores the full locationResponse for later UI rendering.


386-399: The fallback object property naming is correct—the LocationResponseSchema explicitly defines the property as map_commands in snake_case, matching the fallback object structure. No inconsistency exists here.

The console.log on line 390 is consistent with the logging pattern used throughout the file (lines 392, 553, 705, 734, 740), so it's not a unique concern to this location. If production logging needs to be reviewed, it should be done as a broader refactoring across the file rather than isolated to this enrichment flow.

Likely an incorrect or invalid review comment.

app/actions.tsx Outdated
Comment on lines 16 to 17
import { mapControlOrchestrator } from '@/lib/agents/map-control-orchestrator'
import { initializeComposioMapbox } from '@/mapbox_mcp/composio-mapbox'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'mapControlOrchestrator|initializeComposioMapbox' --type=ts --type=tsx -g '!*.test.*'

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

rg -n 'mapControlOrchestrator|initializeComposioMapbox' -t ts -g '!*.test.*'

Repository: QueueLab/QCX

Length of output: 1227


🏁 Script executed:

rg -n 'mapControlOrchestrator\(|initializeComposioMapbox\(' app/actions.tsx

Repository: QueueLab/QCX

Length of output: 38


Remove unused imports: mapControlOrchestrator and initializeComposioMapbox are imported at lines 16-17 but never invoked in this file.

While these functions are used elsewhere in the codebase, they add dead code in app/actions.tsx and should be removed unless they're intended for future use.

🤖 Prompt for AI Agents
In @app/actions.tsx around lines 16 - 17, The file contains unused imports
mapControlOrchestrator and initializeComposioMapbox; remove these two import
statements from app/actions.tsx to eliminate dead code and unused dependencies,
ensuring no other references to mapControlOrchestrator or
initializeComposioMapbox remain in that file and run a quick build/typecheck to
confirm there are no missing usages.

Comment on lines +19 to +28
interface MapFeedback {
success: boolean;
currentCenter: [number, number];
currentZoom: number;
currentPitch: number;
currentBearing: number;
currentBounds?: [[number, number], [number, number]];
error?: string;
timestamp: number;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and read map-schemas file to check MapStateFeedback definition
fd -e ts -e tsx 'map-schemas' --type f

Repository: QueueLab/QCX

Length of output: 80


🏁 Script executed:

#!/bin/bash
# Search for MapStateFeedback definition
rg "MapStateFeedback" --type ts --type tsx -B 2 -A 10

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Search for MapFeedback definition and usage in mapbox-map.tsx
rg "MapFeedback" --type ts --type tsx -B 2 -A 5

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Read the map-schemas.ts file to see MapStateFeedback definition
cat lib/types/map-schemas.ts

Repository: QueueLab/QCX

Length of output: 6885


🏁 Script executed:

#!/bin/bash
# Search for MapStateFeedback definition across the codebase
rg "MapStateFeedback" -B 2 -A 10

Repository: QueueLab/QCX

Length of output: 14276


🏁 Script executed:

#!/bin/bash
# Search for MapFeedback definition and usage
rg "MapFeedback" -B 2 -A 5

Repository: QueueLab/QCX

Length of output: 1634


Replace the local MapFeedback interface with the centralized MapStateFeedback type from lib/types/map-schemas.

The codebase already defines MapStateFeedback as a centralized, schema-validated type used throughout the orchestrator, feedback analyzer, and context layer. Defining a duplicate MapFeedback interface here creates type inconsistency and maintenance overhead. While MapStateFeedback makes several fields optional (which differs from the local interface), the code already provides values for all fields, so this is compatible.

Import MapStateFeedback and remove the local interface definition.

🤖 Prompt for AI Agents
In @components/map/mapbox-map.tsx around lines 19 - 28, Replace the local
MapFeedback interface with the centralized MapStateFeedback type: remove the
MapFeedback interface declaration in mapbox-map.tsx, add an import for
MapStateFeedback from "lib/types/map-schemas", and update any usages or
annotations that reference MapFeedback to use MapStateFeedback instead (no code
logic changes needed since all fields are provided where used).

Comment on lines +559 to +609
// Effect to handle GeoJSON data updates
useEffect(() => {
if (!map.current) return;

const mapInstance = map.current;
const source = mapInstance.getSource('geojson-data');

// If GeoJSON data is present, add or update the source and layers
if (mapData.geojson) {
const geojsonData = mapData.geojson as FeatureCollection;
if (source) {
(source as mapboxgl.GeoJSONSource).setData(geojsonData);
} else {
mapInstance.addSource('geojson-data', {
type: 'geojson',
data: geojsonData,
});

// Add layer for points
mapInstance.addLayer({
id: 'geojson-points',
type: 'circle',
source: 'geojson-data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
},
filter: ['==', '$type', 'Point'],
});

// Add layer for lines
mapInstance.addLayer({
id: 'geojson-lines',
type: 'line',
source: 'geojson-data',
paint: {
'line-color': '#ff4500',
'line-width': 3,
},
filter: ['==', '$type', 'LineString'],
});
}
} else {
// If no GeoJSON data, remove layers and source if they exist
if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
if (source) mapInstance.removeSource('geojson-data');
}
}, [mapData.geojson]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing style-loaded check and polygon layer support.

Two issues with this effect:

  1. Race condition risk: Adding sources/layers without checking map.isStyleLoaded() can throw errors if the style hasn't loaded yet. The geojson-layer.tsx component (from relevant snippets) properly handles this with an isStyleLoaded() check.

  2. Missing polygon support: The existing GeoJsonLayer component handles polygons with fill and outline layers, but this implementation only handles points and lines.

Proposed fix with style-loaded check and polygon support
 // Effect to handle GeoJSON data updates
 useEffect(() => {
   if (!map.current) return;

   const mapInstance = map.current;
-  const source = mapInstance.getSource('geojson-data');

-  // If GeoJSON data is present, add or update the source and layers
-  if (mapData.geojson) {
-    const geojsonData = mapData.geojson as FeatureCollection;
-    if (source) {
-      (source as mapboxgl.GeoJSONSource).setData(geojsonData);
-    } else {
-      mapInstance.addSource('geojson-data', {
-        type: 'geojson',
-        data: geojsonData,
-      });
-
-      // Add layer for points
-      mapInstance.addLayer({
+  const addGeoJSONLayers = () => {
+    const source = mapInstance.getSource('geojson-data');
+    if (mapData.geojson) {
+      const geojsonData = mapData.geojson as FeatureCollection;
+      if (source) {
+        (source as mapboxgl.GeoJSONSource).setData(geojsonData);
+      } else {
+        mapInstance.addSource('geojson-data', {
+          type: 'geojson',
+          data: geojsonData,
+        });
+
+        // Add layer for points
+        mapInstance.addLayer({
           id: 'geojson-points',
           type: 'circle',
           source: 'geojson-data',
           paint: {
             'circle-radius': 8,
             'circle-color': '#007cbf',
             'circle-stroke-width': 2,
             'circle-stroke-color': '#ffffff',
           },
           filter: ['==', '$type', 'Point'],
-      });
+        });

-      // Add layer for lines
-      mapInstance.addLayer({
+        // Add layer for lines
+        mapInstance.addLayer({
           id: 'geojson-lines',
           type: 'line',
           source: 'geojson-data',
           paint: {
             'line-color': '#ff4500',
             'line-width': 3,
           },
           filter: ['==', '$type', 'LineString'],
-      });
+        });
+
+        // Add layer for polygons
+        mapInstance.addLayer({
+          id: 'geojson-polygons',
+          type: 'fill',
+          source: 'geojson-data',
+          paint: {
+            'fill-color': '#088',
+            'fill-opacity': 0.4,
+          },
+          filter: ['==', '$type', 'Polygon'],
+        });
+      }
+    } else {
+      // Remove layers and source if they exist
+      if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
+      if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
+      if (mapInstance.getLayer('geojson-polygons')) mapInstance.removeLayer('geojson-polygons');
+      if (source) mapInstance.removeSource('geojson-data');
     }
-  } else {
-    // If no GeoJSON data, remove layers and source if they exist
-    if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
-    if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
-    if (source) mapInstance.removeSource('geojson-data');
+  };
+
+  if (mapInstance.isStyleLoaded()) {
+    addGeoJSONLayers();
+  } else {
+    mapInstance.once('style.load', addGeoJSONLayers);
   }
 }, [mapData.geojson]);
📝 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.

Suggested change
// Effect to handle GeoJSON data updates
useEffect(() => {
if (!map.current) return;
const mapInstance = map.current;
const source = mapInstance.getSource('geojson-data');
// If GeoJSON data is present, add or update the source and layers
if (mapData.geojson) {
const geojsonData = mapData.geojson as FeatureCollection;
if (source) {
(source as mapboxgl.GeoJSONSource).setData(geojsonData);
} else {
mapInstance.addSource('geojson-data', {
type: 'geojson',
data: geojsonData,
});
// Add layer for points
mapInstance.addLayer({
id: 'geojson-points',
type: 'circle',
source: 'geojson-data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
},
filter: ['==', '$type', 'Point'],
});
// Add layer for lines
mapInstance.addLayer({
id: 'geojson-lines',
type: 'line',
source: 'geojson-data',
paint: {
'line-color': '#ff4500',
'line-width': 3,
},
filter: ['==', '$type', 'LineString'],
});
}
} else {
// If no GeoJSON data, remove layers and source if they exist
if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
if (source) mapInstance.removeSource('geojson-data');
}
}, [mapData.geojson]);
// Effect to handle GeoJSON data updates
useEffect(() => {
if (!map.current) return;
const mapInstance = map.current;
const addGeoJSONLayers = () => {
const source = mapInstance.getSource('geojson-data');
if (mapData.geojson) {
const geojsonData = mapData.geojson as FeatureCollection;
if (source) {
(source as mapboxgl.GeoJSONSource).setData(geojsonData);
} else {
mapInstance.addSource('geojson-data', {
type: 'geojson',
data: geojsonData,
});
// Add layer for points
mapInstance.addLayer({
id: 'geojson-points',
type: 'circle',
source: 'geojson-data',
paint: {
'circle-radius': 8,
'circle-color': '#007cbf',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
},
filter: ['==', '$type', 'Point'],
});
// Add layer for lines
mapInstance.addLayer({
id: 'geojson-lines',
type: 'line',
source: 'geojson-data',
paint: {
'line-color': '#ff4500',
'line-width': 3,
},
filter: ['==', '$type', 'LineString'],
});
// Add layer for polygons
mapInstance.addLayer({
id: 'geojson-polygons',
type: 'fill',
source: 'geojson-data',
paint: {
'fill-color': '#088',
'fill-opacity': 0.4,
},
filter: ['==', '$type', 'Polygon'],
});
}
} else {
// Remove layers and source if they exist
if (mapInstance.getLayer('geojson-points')) mapInstance.removeLayer('geojson-points');
if (mapInstance.getLayer('geojson-lines')) mapInstance.removeLayer('geojson-lines');
if (mapInstance.getLayer('geojson-polygons')) mapInstance.removeLayer('geojson-polygons');
if (source) mapInstance.removeSource('geojson-data');
}
};
if (mapInstance.isStyleLoaded()) {
addGeoJSONLayers();
} else {
mapInstance.once('style.load', addGeoJSONLayers);
}
}, [mapData.geojson]);
🤖 Prompt for AI Agents
In @components/map/mapbox-map.tsx around lines 559 - 609, The effect that
updates the 'geojson-data' source must guard against style not being loaded and
also add polygon support: check map.current and use mapInstance.isStyleLoaded()
(or listen for the 'styledata' event) before calling addSource/addLayer to avoid
race errors, and if waiting for style, retry or attach a one-time listener; when
adding layers, add two polygon layers (e.g., 'geojson-polygons-fill' with type
'fill' and appropriate 'fill-color'/'fill-opacity' and
'geojson-polygons-outline' with type 'line' for stroke) and use filters for
'Polygon' and 'MultiPolygon'; also ensure you remove those polygon layers when
mapData.geojson is falsy and when updating source, mirror existing logic for
points and lines using the same source id 'geojson-data'.

Comment on lines +642 to +651
case 'fitBounds':
if (command.params.bounds) {
const { bounds, padding, duration, ...otherOptions } = command.params;
const fitBoundsOptions: any = {};
if (padding !== undefined) fitBoundsOptions.padding = padding;
if (duration !== undefined) fitBoundsOptions.duration = duration;
Object.assign(fitBoundsOptions, otherOptions);
mapInstance.fitBounds(bounds, fitBoundsOptions);
}
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Minor: fitBoundsOptions is typed as any, losing type safety.

Consider using mapboxgl.FitBoundsOptions for better type checking and IDE support.

-            const fitBoundsOptions: any = {};
+            const fitBoundsOptions: mapboxgl.FitBoundsOptions = {};
📝 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.

Suggested change
case 'fitBounds':
if (command.params.bounds) {
const { bounds, padding, duration, ...otherOptions } = command.params;
const fitBoundsOptions: any = {};
if (padding !== undefined) fitBoundsOptions.padding = padding;
if (duration !== undefined) fitBoundsOptions.duration = duration;
Object.assign(fitBoundsOptions, otherOptions);
mapInstance.fitBounds(bounds, fitBoundsOptions);
}
break;
case 'fitBounds':
if (command.params.bounds) {
const { bounds, padding, duration, ...otherOptions } = command.params;
const fitBoundsOptions: mapboxgl.FitBoundsOptions = {};
if (padding !== undefined) fitBoundsOptions.padding = padding;
if (duration !== undefined) fitBoundsOptions.duration = duration;
Object.assign(fitBoundsOptions, otherOptions);
mapInstance.fitBounds(bounds, fitBoundsOptions);
}
break;
🤖 Prompt for AI Agents
In @components/map/mapbox-map.tsx around lines 642 - 651, The fitBoundsOptions
variable is currently typed as any which loses type safety; change its
declaration to use mapboxgl.FitBoundsOptions (e.g., initialize as {} as
mapboxgl.FitBoundsOptions) and keep assigning padding/duration/otherOptions into
it before calling mapInstance.fitBounds(bounds, fitBoundsOptions); ensure
mapboxgl is available in scope (import or reference the global type) so the
compiler recognizes mapboxgl.FitBoundsOptions and you get IDE/type checking for
the fitBounds call.

Comment on lines +677 to +739
// Guard flag to ensure feedbackHandler only runs once
let handled = false;
let fallbackTimeoutId: NodeJS.Timeout | null = null;

// Capture feedback after commands complete
const feedbackHandler = () => {
if (handled) return;
handled = true;

if (fallbackTimeoutId) {
clearTimeout(fallbackTimeoutId);
}

// Clean up event listener
mapInstance.off('moveend', feedbackHandler);

const center = mapInstance.getCenter();
const bounds = mapInstance.getBounds();

const feedback: MapFeedback = {
success: !executionError,
currentCenter: [center.lng, center.lat],
currentZoom: mapInstance.getZoom(),
currentPitch: mapInstance.getPitch(),
currentBearing: mapInstance.getBearing(),
error: executionError,
timestamp: Date.now(),
};

if (bounds) {
feedback.currentBounds = [
[bounds.getWest(), bounds.getSouth()],
[bounds.getEast(), bounds.getNorth()]
];
}

if (isDebug) console.log('📍 Map feedback:', feedback);

// Send feedback via callback if provided
if (mapData.feedbackCallback) {
mapData.feedbackCallback(feedback);
}

// Store feedback in context
setMapData(prev => ({
...prev,
mapStateFeedback: feedback,
mapCommands: null, // Clear commands after execution
}));
};

// Wait for map to finish moving
mapInstance.once('moveend', feedbackHandler);

// Fallback timeout in case moveend doesn't fire
// Increased timeout to allow for animation start
fallbackTimeoutId = setTimeout(() => {
if (mapInstance.isMoving && mapInstance.isMoving()) {
return; // Still moving, wait for moveend
}
// If not moving, trigger feedback immediately
feedbackHandler();
}, 500);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Memory leak: Missing cleanup for timeout and event listener.

The fallbackTimeoutId timeout and moveend event listener are not cleaned up when the effect re-runs or unmounts. This can cause memory leaks and stale callbacks executing on an unmounted component.

Proposed fix with proper cleanup
   // Effect to execute map commands with feedback
   useEffect(() => {
+    let fallbackTimeoutId: NodeJS.Timeout | null = null;
+    let handled = false;
+
     if (isDebug) {
       console.log('🗺️  Map commands useEffect triggered:', {
         // ...
       });
     }

     if (!map.current || !mapData.mapCommands || mapData.mapCommands.length === 0) {
       if (isDebug) console.log('⚠️ Skipping command execution - missing map or commands');
       return;
     }

     const mapInstance = map.current;
     let executionError: string | undefined;

     // ... command execution code ...

     try {
       // ... forEach loop ...

-      // Guard flag to ensure feedbackHandler only runs once
-      let handled = false;
-      let fallbackTimeoutId: NodeJS.Timeout | null = null;

       // Capture feedback after commands complete
       const feedbackHandler = () => {
         // ... existing handler code ...
       };

       // Wait for map to finish moving
       mapInstance.once('moveend', feedbackHandler);
       
       // Fallback timeout
       fallbackTimeoutId = setTimeout(() => {
         // ... existing timeout code ...
       }, 500);

     } catch (error) {
       // ... error handling ...
     }

+    // Cleanup function
+    return () => {
+      if (fallbackTimeoutId) {
+        clearTimeout(fallbackTimeoutId);
+      }
+      if (!handled) {
+        mapInstance.off('moveend', feedbackHandler);
+      }
+    };

   }, [mapData.mapCommands, mapData.feedbackCallback, setMapData, isDebug]);

Note: The feedbackHandler function needs to be declared outside the try block for the cleanup to reference it properly, or use a ref to store it.

🤖 Prompt for AI Agents
In @components/map/mapbox-map.tsx around lines 677 - 739, The effect can leak
because fallbackTimeoutId and the moveend listener referencing feedbackHandler
aren't cleaned up on re-run/unmount; declare feedbackHandler and
fallbackTimeoutId in stable scope (or store them in refs) so the cleanup can
access the same references, and in the effect return
clearTimeout(fallbackTimeoutId) and remove the listener via
mapInstance.off('moveend', feedbackHandler) (also set handled = true or similar
guard) to ensure the timeout and event listener are always removed; adjust
placement so feedbackHandler is defined outside try/async blocks or use useRef
to hold the handler before registering mapInstance.once.

@ngoiyaeric
Copy link
Collaborator Author

@coderabbitai Make sure you make the map language controlled via composio to enable location rendering. Give examples of queries that should work perfectly

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 13, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@ngoiyaeric, I'll analyze the current state and plan the code changes needed to make the map fully language-controlled via Composio. Let me check the integration points first.


🏁 Script executed:

#!/bin/bash

echo "=== 1. Checking if enricher is called in app/actions.tsx ==="
rg -n "geojsonEnrich" app/actions.tsx -B 5 -A 10

echo -e "\n=== 2. Checking orchestrator usage ==="
rg -n "mapControlOrchestrator|orchestrator" app/actions.tsx -B 3 -A 3

echo -e "\n=== 3. Checking Composio initialization ==="
cat mapbox_mcp/composio-mapbox.ts | head -80

echo -e "\n=== 4. Checking geospatial tool Composio usage ==="
cat lib/agents/tools/geospatial.tsx | grep -A 30 "getConnectedMcpClient"

echo -e "\n=== 5. Checking submit function in actions.tsx ==="
rg -n "export async function submit" app/actions.tsx -A 100 | head -120

echo -e "\n=== 6. Checking researcher tool integration ==="
rg -n "const.*researcher|researcher.*=" app/actions.tsx -B 2 -A 8 | head -40

echo -e "\n=== 7. Checking if LocationResponseHandler is rendered ==="
rg -n "LocationResponseHandler" app/actions.tsx -B 2 -A 5

Length of output: 10226


[plan_code_changes]

Perfect! I can see the enricher IS being called at line 389, but WITHOUT the Composio client. The orchestrator (which has full MCP integration) is imported but never used. Let me plan the code changes to enable full language-controlled map rendering via Composio.

Implementation Plan: Enable Composio-Powered Map Control

Current Flow (Limited):

researcher → answer → geojsonEnricherV2(answer) → LLM hallucinates coordinates → map
                                      ↑
                                 NO MCP CLIENT!

Fixed Flow (Composio-Powered):

researcher → answer → mapControlOrchestrator(answer, { mcpClient }) → 
                                ↓
                    Composio geocodes → validates → generates commands → map


🎯 Example Queries That Will Work Perfectly

After these changes, the following queries will work with accurate Composio-powered geocoding:

✅ Single Location Queries

  1. "Show me Paris"

    • Composio geocodes "Paris" → accurate coordinates (2.3522, 48.8566)
    • Generates flyTo command → smooth animation to Paris
  2. "Take me to the Eiffel Tower"

    • Composio geocodes "Eiffel Tower" → precise location (48.8584, 2.2945)
    • Generates flyTo with zoom 17 → zooms into the landmark
  3. "Fly to 1600 Pennsylvania Avenue, Washington DC"

    • Composio geocodes full address → exact White House coordinates
    • Generates flyTo command → flies to the address

✅ Multiple Location Queries

  1. "Show me Tokyo, London, and New York"

    • Composio geocodes all three cities → accurate coordinates for each
    • Generates 3 Point features in GeoJSON
    • Generates fitBounds command → fits all cities in view
  2. "Compare San Francisco and Seattle"

    • Geocodes both cities via Composio
    • Creates Point features with city names in properties
    • Uses fitBounds to show both

✅ Route & Distance Queries

  1. "Show me the route from Boston to Miami"

    • Composio geocodes both cities
    • Calls mapbox_directions action via Composio
    • Creates LineString GeoJSON from route
    • Uses fitBounds to show entire route
  2. "What's the distance from NYC to LA?"

    • Composio geocodes both cities
    • Calls mapbox_matrix action for distance calculation
    • Displays distance in text response
    • Creates LineString connecting the cities

✅ Search & Discovery Queries

  1. "Find restaurants near Times Square"

    • Composio geocodes "Times Square"
    • Calls mapbox_search_box_text_search with "restaurants" query
    • Creates Point features for each result
    • Uses fitBounds to show all results
  2. "Show me coffee shops in downtown Seattle"

    • Geocodes "downtown Seattle"
    • Searches for coffee shops via Composio
    • Plots all results on map with markers

✅ Advanced 3D View Queries

  1. "Give me a 3D view of Manhattan"

    • Geocodes "Manhattan"
    • Generates flyTo with pitch: 60 for tilted view
    • Zooms to appropriate level for city view
  2. "Tilt the view to 45 degrees and rotate to face east"

    • Generates setPitch with pitch: 45
    • Generates setBearing with bearing: 90 (east)
    • Executes commands in sequence

✅ Reverse Geocoding

  1. "What's at coordinates 40.7128, -74.0060?"
    • Calls mapbox_reverse_geocoding via Composio
    • Returns location name (Lower Manhattan, NYC)
    • Generates flyTo to the coordinates

🚀 What Makes This Work

Before (Current):

❌ LLM hallucinates coordinates from text
❌ No validation of addresses
❌ No distance calculations
❌ No place search
❌ Limited command set

After (With Composio):

Accurate geocoding via Composio SDK
Real distance calculations with Mapbox Matrix API
Place search with Mapbox Search API
Route generation with Mapbox Directions API
Validation with Zod schemas
Feedback loops for iterative refinement
Full command set (flyTo, easeTo, fitBounds, setCenter, setZoom, setPitch, setBearing)


📊 Expected Performance

  • Single location: 1-2 seconds (geocode + render)
  • Multiple locations: 2-3 seconds (multiple geocodes + render)
  • Routes: 3-5 seconds (geocode + directions + render)
  • Search queries: 2-4 seconds (geocode + search + render)
  • With feedback loops: 4-8 seconds (up to 3 iterations for refinement)

All queries will have accurate, real-world coordinates from Composio instead of LLM-hallucinated data! 🎯

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/actions.tsx (1)

644-654: Wrap switch case declaration in a block.

The static analysis tool (Biome) correctly identifies that declarations in switch cases can leak to other cases, potentially causing unexpected behavior.

🐛 Proposed fix: wrap in block scope
-            case 'related':
-              const relatedQueries = createStreamableValue<PartialRelated>()
-              relatedQueries.done(JSON.parse(content as string))
-              return {
-                id,
-                component: (
-                  <Section title="Related" separator={true}>
-                    <SearchRelated relatedQueries={relatedQueries.value} />
-                  </Section>
-                )
-              }
+            case 'related': {
+              const relatedQueries = createStreamableValue<PartialRelated>()
+              relatedQueries.done(JSON.parse(content as string))
+              return {
+                id,
+                component: (
+                  <Section title="Related" separator={true}>
+                    <SearchRelated relatedQueries={relatedQueries.value} />
+                  </Section>
+                )
+              }
+            }
🤖 Fix all issues with AI agents
In @lib/agents/map-control-orchestrator.tsx:
- Around line 43-48: The OrchestratorOptions currently types mcpClient as any,
losing type safety; import the proper client type from the Composio SDK (e.g.,
ComposioClient or whatever exported client type) and replace mcpClient?: any
with mcpClient?: ComposioClient (or the SDK type), or if the SDK type is
unavailable create a local interface (e.g., MpcClientLike) describing the
methods you use and use that type; update all references to
OrchestratorOptions/mcpClient to use the new type so you regain IDE completions
and compile-time checks.
- Around line 110-136: The code overwrites geocoded features because
this.state.geojson is set from parserOutput.geojson after calling
geocodeLocations; move the assignment this.state.geojson = parserOutput.geojson
to before calling this.geocodeLocations(parserOutput.extractedLocations), and
ensure geocodeLocations either mutates this.state.geojson or returns merged
geocoded features to be merged into this.state.geojson (update the call to use
the returned value if it returns features). After that, run
validateGeoJSON(this.state.geojson) as before to validate the final merged
GeoJSON.
- Around line 91-94: The process method on MapControlOrchestrator declares a
feedbackCallback parameter but never invokes it; either remove the parameter or
call feedbackCallback with a MapStateFeedback when processing completes or at
key iteration points. Locate the async process(researcherResponse,
feedbackCallback?) method and, if keeping the callback, construct and pass a
MapStateFeedback object (e.g., final status, updated map state, progress or
error) to feedbackCallback(...) before returning the LocationResponse (and
optionally at intermediate steps), or delete the feedbackCallback parameter and
all related references to eliminate the unused argument.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f989c2 and 05f093d.

📒 Files selected for processing (2)
  • app/actions.tsx
  • lib/agents/map-control-orchestrator.tsx
🧰 Additional context used
🪛 Biome (2.1.2)
app/actions.tsx

[error] 645-646: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (12)
lib/agents/map-control-orchestrator.tsx (6)

1-41: LGTM!

The imports and classification prompt are well-structured. The prompt provides clear classification types, complexity levels, and MCP operations guidance.


63-86: LGTM!

The constructor properly initializes defaults and state. The pattern of merging user-provided options with defaults is clean.


147-160: LGTM!

Command validation with filtering of invalid commands is a good defensive pattern. The filtering relies on error field naming convention, which works but could be more robust with structured error indices.


279-300: LGTM!

The classification method has proper error handling with a conservative fallback that defaults to simple_location with requiresMCP: false, ensuring the system degrades gracefully.


366-440: LGTM!

The geocodeLocations method handles multiple Composio response formats robustly and correctly merges geocoded features into the existing GeoJSON state.


446-452: LGTM!

The convenience helper function provides a clean API for simple usage without managing orchestrator instances.

app/actions.tsx (6)

16-33: LGTM!

The new imports are properly organized and all appear to be used in the implementation.


72-72: LGTM!

Storing multimodal content as a JSON string ensures proper serialization for the AI state.


290-290: Type assertion without runtime validation.

The cast as PartialInquiry assumes inquire() returns a compatible structure. Consider adding a runtime type guard if the return type isn't guaranteed.


427-457: LGTM!

The AI state update correctly stores the LocationResponse as a tool message, enabling proper UI rendering in getUIStateFromAIState. Using locationResponse.text for the assistant content maintains consistency with the enriched response.


703-722: LGTM!

The validation logic correctly checks the LocationResponse structure before rendering. The guard ensures text is a string and geojson is either null or a valid FeatureCollection shape, preventing runtime errors in LocationResponseHandler.


376-416: Robust fallback chain for map enrichment.

The implementation has a solid three-tier fallback:

  1. Full orchestrator with MCP
  2. Basic enricher v2
  3. Minimal response with null geojson

The environment variable check at line 383 prevents unnecessary initialization attempts. The initializeComposioMapbox function correctly returns an object with both composio and connectionId properties as expected by the consumer code.

@ngoiyaeric
Copy link
Collaborator Author

@coderabbitai 15:54:20.087 Running build in Portland, USA (West) – pdx1
15:54:20.087 Build machine configuration: 2 cores, 8 GB
15:54:20.204 Cloning github.com/QueueLab/QCX (Branch: feat/geojson-enrichment-agent, Commit: f8c49d2)
15:54:22.673 Cloning completed: 2.469s
15:54:22.818 Restored build cache from previous deployment (59sZ6x5yRw8iFKbf1u72J4tXXtDr)
15:54:23.114 Running "vercel build"
15:54:23.580 Vercel CLI 50.1.6
15:54:23.913 Running "install" command: bun install...
15:54:23.942 [0.46ms] ".env"
15:54:23.945 bun install v1.3.5 (1e86cebd)
15:54:24.301 Saved lockfile
15:54:24.302
15:54:24.302 Checked 1302 installs across 1316 packages (no changes) [369.00ms]
15:54:24.305 Detected Next.js version: 15.3.6
15:54:24.306 Running "bun run build"
15:54:24.311 $ node --max-old-space-size=4096 node_modules/.bin/next build
15:54:25.171 ▲ Next.js 15.3.6
15:54:25.172 - Environments: .env
15:54:25.172
15:54:25.220 Creating an optimized production build ...
15:54:49.197 ✓ Compiled successfully in 20.0s
15:54:49.202 Linting and checking validity of types ...
15:54:57.440
15:54:57.440 ./components/chat-panel.tsx
15:54:57.441 156:5 Warning: React Hook useCallback has a missing dependency: 'setSuggestions'. Either include it or remove the dependency array. react-hooks/exhaustive-deps
15:54:57.441
15:54:57.441 ./components/map/mapbox-map.tsx
15:54:57.441 529:6 Warning: React Hook useEffect has missing dependencies: 'mapData.cameraState', 'position?.latitude', and 'position?.longitude'. Either include them or remove the dependency array. react-hooks/exhaustive-deps
15:54:57.441 767:6 Warning: React Hook useEffect has a missing dependency: 'mapData'. Either include it or remove the dependency array. react-hooks/exhaustive-deps
15:54:57.441
15:54:57.441 info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/app/api-reference/config/eslint#disabling-rules
15:55:08.195 Failed to compile.
15:55:08.195
15:55:08.195 ./app/actions.tsx:399:13
15:55:08.195 Type error: Type 'Composio | null' is not assignable to type 'Composio | undefined'.
15:55:08.195 Type 'null' is not assignable to type 'Composio | undefined'.
15:55:08.195
15:55:08.196 �[0m �[90m 397 |�[39m console�[33m.�[39mlog(�[32m'🚀 Starting Map Control Orchestrator...'�[39m)�[33m;�[39m�[0m
15:55:08.196 �[0m �[90m 398 |�[39m locationResponse �[33m=�[39m �[36mawait�[39m mapControlOrchestrator(answer�[33m,�[39m {�[0m
15:55:08.196 �[0m�[31m�[1m>�[22m�[39m�[90m 399 |�[39m mcpClient�[33m,�[39m�[0m
15:55:08.196 �[0m �[90m |�[39m �[31m�[1m^�[22m�[39m�[0m
15:55:08.196 �[0m �[90m 400 |�[39m connectionId�[33m,�[39m�[0m
15:55:08.196 �[0m �[90m 401 |�[39m enableFeedbackLoop�[33m:�[39m �[36mfalse�[39m �[90m// Disable for now to reduce latency�[39m�[0m
15:55:08.196 �[0m �[90m 402 |�[39m })�[33m;�[39m�[0m
15:55:08.237 Next.js build worker exited with code: 1 and signal: null
15:55:08.243 error: script "build" exited with code 1
15:55:08.254 Error: Command "bun run build" exited with 1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @lib/agents/map-control-orchestrator.tsx:
- Around line 328-349: classifyQuery uses this.model which is a Promise, causing
generateObject to receive an unresolved promise; before calling generateObject
inside classifyQuery await the model (e.g., const model = await this.model) and
pass that resolved model to generateObject so generateObject gets the actual
model instance; update references in classifyQuery to use the resolved model
variable.
- Around line 380-400: The placeholders for 'calculate_distance' and
'generate_map_link' in MapControlOrchestrator mean queries classified with
requiresMCP: true and mcpOperations including 'calculate_distance' won't perform
routing; either implement calculate_distance (use
this.options.mcpClient.executeAction with a real MCP action such as
'mapbox_directions' or 'mapbox_matrix', pass origin/destination coords, profile,
and parse distance/duration into result) and implement generate_map_link
(construct a Mapbox/static or directions URL from coords and include it in
result), or change classifyQuery so it doesn't mark route_distance queries as
requiresMCP or include 'calculate_distance' in mcpOperations; update the code
paths referencing calculate_distance and generate_map_link in
map-control-orchestrator (and any tests) accordingly so the classifier and
executor stay consistent.
- Around line 173-186: The current filtering of this.state.commands is brittle
because it parses validation.errors field names (commands[${idx}]) from
validateMapCommands; change validateMapCommands to return explicit invalid
command indices (e.g., invalidIndices: number[]) or a per-command boolean array,
and update the call in map-control-orchestrator to use that property when
filtering (use validation.invalidIndices to remove invalid commands and preserve
others); also keep logging validation.errors/validation.warnings but stop
depending on string parsing of error.field in this.state.commands filtering
logic.
- Around line 52-59: OrchestratorState currently types mcpData as any[], which
loses type safety; define a new interface (e.g., MCPResult or McpDataItem) that
models the MCP result shape (fields returned/consumed by map-control logic) and
replace mcpData: any[] with mcpData: MCPResult[] in the OrchestratorState
definition, then update any functions, variables, and places that construct or
read mcpData (search for usages of mcpData and MapControlOrchestrator-related
handlers) to use the new type and adjust property accesses as needed to satisfy
the compiler.
- Around line 65-96: getModel() is async but the constructor assigns it
synchronously to this.model, so this.model ends up as a Promise and breaks
classifyQuery; change the initialization to resolve the model asynchronously
(e.g., add a static async MapControlOrchestrator.create(options) or an async
init() that awaits getModel() and sets this.model) and ensure classifyQuery uses
the resolved model (or awaits a ready promise) before calling generateObject;
also fix types by making OrchestratorOptions.mcpClient nullable (Composio |
null) and stop forcing Required<OrchestratorOptions> with as any casts so the
constructor can accept and explicitly set mcpClient = null when absent.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05f093d and f8c49d2.

📒 Files selected for processing (1)
  • lib/agents/map-control-orchestrator.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
lib/agents/map-control-orchestrator.tsx (5)
lib/types/map-schemas.ts (7)
  • QueryClassification (222-222)
  • GeoJSONFeatureCollection (214-214)
  • MapCommand (215-215)
  • MapStateFeedback (217-217)
  • LocationResponse (223-223)
  • QueryClassificationSchema (179-196)
  • GeoJSONFeature (213-213)
lib/utils/index.ts (1)
  • getModel (20-121)
lib/agents/map-workers/geojson-parser.tsx (1)
  • geojsonParser (72-111)
lib/agents/map-workers/map-command-generator.tsx (1)
  • mapCommandGenerator (73-155)
lib/agents/map-workers/feedback-analyzer.tsx (1)
  • feedbackAnalyzer (50-138)
🔇 Additional comments (5)
lib/agents/map-control-orchestrator.tsx (5)

1-16: Imports look correct.

The imports are properly structured, pulling from the correct schema files and worker modules. The Composio type import from @composio/core aligns with the library context.


18-43: Prompt aligns with schema definitions.

The classification types and MCP operations match the QueryClassificationSchema enums. Clear and well-structured.


248-323: Feedback processing logic is well-structured.

The iteration limit check and action-based switch handling are appropriate. The logging provides good observability for debugging.


419-474: Geocoding implementation handles multiple response formats well.

The method correctly handles both data.features and data.location response structures from the Composio API. Error handling per location prevents one failure from stopping all geocoding.


203-212: Initial feedback notification is intentional; full map state is sent by the Mapbox component.

The minimal MapStateFeedback sent at lines 205–209 is a status notification that initial processing completed—not the actual map state feedback. The complete feedback with currentCenter, currentZoom, and currentBounds is sent by the mapbox-map.tsx component after executing commands (see mapbox-map.tsx:716–717). The feedback loop is two-stage: orchestrator sends an initial success/error status, then the map component sends full state data when moveend fires. This design is correct.

Consider adding a comment clarifying that this is an initial status notification, and the full map state feedback is populated by the Mapbox component to make the two-stage flow explicit.

Comment on lines 65 to 96
export class MapControlOrchestrator {
private options: Required<OrchestratorOptions>;
private state: OrchestratorState;
private model: any;

constructor(options: OrchestratorOptions = {}) {
this.options = {
maxIterations: options.maxIterations || 3,
enableFeedbackLoop: options.enableFeedbackLoop ?? true,
mcpClient: options.mcpClient as Composio, // Casting to Required<T> type but it can be null internally if passed as null.
// Actually strictly speaking Required<OrchestratorOptions> would require mcpClient to be Composio.
// But we default it to null in constructor options.
// Let's adjust strict typing logic.
connectionId: options.connectionId || '',
} as any; // Cast to bypass strict null checks for optional fields in Required

// Handle null explicitly
if (!options.mcpClient) {
(this.options as any).mcpClient = null;
}

this.state = {
iteration: 0,
classification: null,
geojson: null,
commands: [],
mcpData: [],
feedback: null,
};

this.model = getModel();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: getModel() is async but not awaited.

getModel() returns a Promise (see lib/utils/index.ts lines 19-120), but line 95 assigns it synchronously. This means this.model holds a Promise, not the resolved model, causing generateObject to fail at runtime in classifyQuery.

Additionally, the as any casts on lines 74-79 and 83 indicate a type design issue. Consider making mcpClient explicitly nullable instead of using Required<T> with type escapes.

🐛 Proposed fix
 export class MapControlOrchestrator {
-  private options: Required<OrchestratorOptions>;
+  private options: {
+    maxIterations: number;
+    enableFeedbackLoop: boolean;
+    mcpClient: Composio | null;
+    connectionId: string;
+  };
   private state: OrchestratorState;
   private model: any;
+  private modelPromise: Promise<any>;

   constructor(options: OrchestratorOptions = {}) {
     this.options = {
       maxIterations: options.maxIterations || 3,
       enableFeedbackLoop: options.enableFeedbackLoop ?? true,
-      mcpClient: options.mcpClient as Composio,
-      connectionId: options.connectionId || '',
-    } as any;
-
-    if (!options.mcpClient) {
-        (this.options as any).mcpClient = null;
-    }
+      mcpClient: options.mcpClient || null,
+      connectionId: options.connectionId || '',
+    };

     this.state = {
       iteration: 0,
       classification: null,
       geojson: null,
       commands: [],
       mcpData: [],
       feedback: null,
     };

-    this.model = getModel();
+    this.modelPromise = getModel();
+  }
+
+  private async ensureModel(): Promise<any> {
+    if (!this.model) {
+      this.model = await this.modelPromise;
+    }
+    return this.model;
   }

Then update classifyQuery to use the resolved model:

   private async classifyQuery(text: string): Promise<QueryClassification> {
     try {
+      const model = await this.ensureModel();
       const { object } = await generateObject({
-        model: this.model,
+        model,
         schema: QueryClassificationSchema,
🤖 Prompt for AI Agents
In @lib/agents/map-control-orchestrator.tsx around lines 65 - 96, getModel() is
async but the constructor assigns it synchronously to this.model, so this.model
ends up as a Promise and breaks classifyQuery; change the initialization to
resolve the model asynchronously (e.g., add a static async
MapControlOrchestrator.create(options) or an async init() that awaits getModel()
and sets this.model) and ensure classifyQuery uses the resolved model (or awaits
a ready promise) before calling generateObject; also fix types by making
OrchestratorOptions.mcpClient nullable (Composio | null) and stop forcing
Required<OrchestratorOptions> with as any casts so the constructor can accept
and explicitly set mcpClient = null when absent.

Comment on lines 380 to 400
case 'calculate_distance':
// Placeholder for matrix/directions
break;

case 'search_nearby':
// Placeholder for search
if (this.options.connectionId) {
result = await this.options.mcpClient.executeAction({
action: 'mapbox_search_box_text_search',
params: {
q: text
},
connectedAccountId: this.options.connectionId
});
}
break;

case 'generate_map_link':
// Would need coordinates
// Skipping for now
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Placeholder MCP operations may leave functionality incomplete.

calculate_distance (line 381) and generate_map_link (line 398) are placeholders. If route_distance queries classify with requiresMCP: true and mcpOperations: ['calculate_distance'], no actual routing occurs.

Consider either implementing these or updating classifyQuery to avoid suggesting unsupported operations.

Would you like me to help implement the calculate_distance operation using Composio's matrix or directions API?

🤖 Prompt for AI Agents
In @lib/agents/map-control-orchestrator.tsx around lines 380 - 400, The
placeholders for 'calculate_distance' and 'generate_map_link' in
MapControlOrchestrator mean queries classified with requiresMCP: true and
mcpOperations including 'calculate_distance' won't perform routing; either
implement calculate_distance (use this.options.mcpClient.executeAction with a
real MCP action such as 'mapbox_directions' or 'mapbox_matrix', pass
origin/destination coords, profile, and parse distance/duration into result) and
implement generate_map_link (construct a Mapbox/static or directions URL from
coords and include it in result), or change classifyQuery so it doesn't mark
route_distance queries as requiresMCP or include 'calculate_distance' in
mcpOperations; update the code paths referencing calculate_distance and
generate_map_link in map-control-orchestrator (and any tests) accordingly so the
classifier and executor stay consistent.

Comment on lines +480 to +486
export async function mapControlOrchestrator(
researcherResponse: string,
options?: OrchestratorOptions
): Promise<LocationResponse> {
const orchestrator = new MapControlOrchestrator(options);
return orchestrator.process(researcherResponse);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Convenience function omits feedbackCallback parameter.

The exported mapControlOrchestrator function doesn't pass a feedbackCallback to process(), so callers using this helper cannot receive feedback. Consider extending the options or signature to support feedback.

♻️ Suggested improvement
+interface MapControlOrchestratorOptions extends OrchestratorOptions {
+  feedbackCallback?: (feedback: MapStateFeedback) => void;
+}
+
 export async function mapControlOrchestrator(
   researcherResponse: string,
-  options?: OrchestratorOptions
+  options?: MapControlOrchestratorOptions
 ): Promise<LocationResponse> {
   const orchestrator = new MapControlOrchestrator(options);
-  return orchestrator.process(researcherResponse);
+  return orchestrator.process(researcherResponse, options?.feedbackCallback);
 }
📝 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.

Suggested change
export async function mapControlOrchestrator(
researcherResponse: string,
options?: OrchestratorOptions
): Promise<LocationResponse> {
const orchestrator = new MapControlOrchestrator(options);
return orchestrator.process(researcherResponse);
}
interface MapControlOrchestratorOptions extends OrchestratorOptions {
feedbackCallback?: (feedback: MapStateFeedback) => void;
}
export async function mapControlOrchestrator(
researcherResponse: string,
options?: MapControlOrchestratorOptions
): Promise<LocationResponse> {
const orchestrator = new MapControlOrchestrator(options);
return orchestrator.process(researcherResponse, options?.feedbackCallback);
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/actions.tsx (1)

645-655: Wrap switch case declaration in a block.

The relatedQueries constant declaration inside the 'related' case can leak to other switch clauses. Static analysis correctly flags this.

Proposed fix
-            case 'related':
-              const relatedQueries = createStreamableValue<PartialRelated>()
-              relatedQueries.done(JSON.parse(content as string))
-              return {
-                id,
-                component: (
-                  <Section title="Related" separator={true}>
-                    <SearchRelated relatedQueries={relatedQueries.value} />
-                  </Section>
-                )
-              }
+            case 'related': {
+              const relatedQueries = createStreamableValue<PartialRelated>()
+              relatedQueries.done(JSON.parse(content as string))
+              return {
+                id,
+                component: (
+                  <Section title="Related" separator={true}>
+                    <SearchRelated relatedQueries={relatedQueries.value} />
+                  </Section>
+                ),
+              }
+            }
🤖 Fix all issues with AI agents
In @lib/agents/map-control-orchestrator.tsx:
- Around line 423-425: The current cast of result to any (resAny) hides the real
Composio tool response shape; define a proper type/interface (e.g.,
ComposioExecutionResult) that models the expected fields (like data, error,
etc.), replace the any assertion with a narrow cast to that type, and extract
data via const data = (result as ComposioExecutionResult).data ?? result to
preserve compatibility; update any related helpers/types and adjust consumers of
data accordingly so type-checking enforces the Composio response structure
instead of using resAny.
- Around line 381-389: The 'generate_map_link' switch case declares a const link
which can leak into other cases; wrap the case body in its own block scope (add
{ ... } after case 'generate_map_link': and before break) so the const link is
scoped locally and cannot conflict with other switch clauses — update the case
block that builds the Mapbox URL and assigns result accordingly (symbols: case
'generate_map_link', const link, result).

In @lib/agents/map-workers/validator.tsx:
- Around line 187-192: The zoom-range check is unreachable because
MapCommandParamsSchema enforces zoom via z.number().min(0).max(22); remove the
conditional block that inspects cmd.params.zoom and pushes the `Unusual zoom
level` warning (the if (cmd.params.zoom !== undefined) { if (cmd.params.zoom < 0
|| cmd.params.zoom > 22) { warnings.push(...) } } block) or replace it with a
different runtime check if you intend to warn about other conditions; ensure no
other code expects that warning to be produced when relying on Zod validation.

In @lib/types/map-schemas.ts:
- Around line 127-141: MapStateFeedbackSchema currently accepts raw numbers for
currentBounds and currentCenter; add explicit latitude/longitude range
validation to match other schemas by replacing z.number() inside the coordinate
tuples with z.number().min(-180).max(180) for longitude and
z.number().min(-90).max(90) for latitude (or the project’s chosen order) so both
currentCenter (z.tuple([lon, lat])) and each coordinate in currentBounds’ nested
tuples are validated; update MapStateFeedbackSchema to use those bounded
z.number() validators for all coordinate entries.
- Around line 98-99: The bearing schema currently uses
z.number().min(0).max(360).optional(), which wrongly rejects valid negative
bearings; update the bearing validator in map-schemas (the bearing field next to
pitch) to allow the full Mapbox range by changing its bounds to min(-180) and
max(360) (keeping .optional()), so negative bearings like -45 are accepted.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8c49d2 and 4ea8401.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • app/actions.tsx
  • lib/agents/map-control-orchestrator.tsx
  • lib/agents/map-workers/validator.tsx
  • lib/types/map-schemas.ts
🧰 Additional context used
🧬 Code graph analysis (1)
lib/agents/map-workers/validator.tsx (1)
lib/types/map-schemas.ts (5)
  • ValidationResult (229-229)
  • GeoJSONFeatureCollectionSchema (86-89)
  • GeoJSONFeatureCollection (222-222)
  • MapCommandSchema (119-125)
  • MapCommand (223-223)
🪛 Biome (2.1.2)
app/actions.tsx

[error] 646-647: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

lib/agents/map-control-orchestrator.tsx

[error] 384-386: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (4)
lib/agents/map-control-orchestrator.tsx (1)

66-89: LGTM on class structure and initialization.

The orchestrator class properly handles default options, initializes state, and defers model resolution. The pattern of storing getModel() as a promise and awaiting it in classifyQuery() is correct.

lib/agents/map-workers/validator.tsx (1)

228-262: LGTM on batch command validation.

The validateMapCommands function properly aggregates per-command validation results, prefixes field paths with indices, and tracks invalid indices for downstream filtering.

app/actions.tsx (2)

376-417: LGTM on orchestrator integration with proper fallback chain.

The integration correctly:

  1. Checks for required environment variables before Composio initialization
  2. Handles Composio initialization failures gracefully
  3. Falls back to geojsonEnricherV2 if orchestration fails
  4. Provides a minimal fallback if even the enricher fails
  5. Logs appropriately at each step

704-723: LGTM on defensive validation for geojsonEnrichment.

Good defensive checks before rendering LocationResponseHandler:

  • Verifies text is a string
  • Validates geojson is either null or a valid FeatureCollection
  • Logs a warning and returns null for malformed data

Comment on lines +346 to +370
case 'calculate_distance':
// Implement calculate_distance
// Extract potential locations? This is hard without parsing "A to B"
// For now, we assume text contains enough info, or we skip if not structured.
// Using mapbox_directions is an option if we can parse waypoints.
// As a fallback/placeholder that fulfills the requirement:
console.log('Attempting calculate_distance via Mapbox Directions...');
// We'd need to extract origin/dest.
// Let's try to just pass the text as query if tool supports it?
// mapbox_directions takes 'waypoints'.
// If we can't parse, we might skip.
// But prompt says "pass origin/destination coords...". The orchestrator doesn't have them yet unless from geocode.
// I'll leave a log and dummy result or try a broader search if applicable.
// ACTUALLY, I'll use mapbox_search_box_text_search as a proxy if I can't do directions, OR just skip.
// But the prompt wants it implemented.
// I will mock it for now or assume we extracted them.
// Since I can't easily parse "A to B" reliably without another LLM call or regex, I will skip with a log,
// BUT strictly I should use executeAction.
// I'll try to find if there is a 'natural_language_directions' tool? No.

// I will construct a Mapbox Directions URL as a result to satisfy "calculate_distance" somewhat?
// No, that's generate_map_link.

// I'll try to geocode the whole text and see if it gives route? Unlikely.
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

calculate_distance operation is unimplemented.

The calculate_distance case contains extensive comments explaining the difficulty but performs no actual operation. This results in silent failures when the orchestrator classifies a query as needing distance calculation.

At minimum, log a warning and consider returning a structured "not implemented" result, or implement the operation using mapbox_directions with parsed waypoints.

Suggested minimal fix
          case 'calculate_distance':
-             // ... extensive comments ...
-             break;
+            console.warn('calculate_distance operation not yet implemented - skipping');
+            result = {
+              error: 'calculate_distance not implemented',
+              description: 'Route/distance calculation requires waypoint parsing - not yet supported'
+            };
+            break;
📝 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.

Suggested change
case 'calculate_distance':
// Implement calculate_distance
// Extract potential locations? This is hard without parsing "A to B"
// For now, we assume text contains enough info, or we skip if not structured.
// Using mapbox_directions is an option if we can parse waypoints.
// As a fallback/placeholder that fulfills the requirement:
console.log('Attempting calculate_distance via Mapbox Directions...');
// We'd need to extract origin/dest.
// Let's try to just pass the text as query if tool supports it?
// mapbox_directions takes 'waypoints'.
// If we can't parse, we might skip.
// But prompt says "pass origin/destination coords...". The orchestrator doesn't have them yet unless from geocode.
// I'll leave a log and dummy result or try a broader search if applicable.
// ACTUALLY, I'll use mapbox_search_box_text_search as a proxy if I can't do directions, OR just skip.
// But the prompt wants it implemented.
// I will mock it for now or assume we extracted them.
// Since I can't easily parse "A to B" reliably without another LLM call or regex, I will skip with a log,
// BUT strictly I should use executeAction.
// I'll try to find if there is a 'natural_language_directions' tool? No.
// I will construct a Mapbox Directions URL as a result to satisfy "calculate_distance" somewhat?
// No, that's generate_map_link.
// I'll try to geocode the whole text and see if it gives route? Unlikely.
break;
case 'calculate_distance':
console.warn('calculate_distance operation not yet implemented - skipping');
result = {
error: 'calculate_distance not implemented',
description: 'Route/distance calculation requires waypoint parsing - not yet supported'
};
break;

Comment on lines +381 to +389
case 'generate_map_link':
// Generate a static map link or similar
// We can return a result object with the link
const link = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/${encodeURIComponent(text)}/600x400?access_token=${process.env.MAPBOX_ACCESS_TOKEN}`;
result = {
url: link,
description: "Static map link generated"
};
break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Wrap switch case declaration in a block.

The link constant declaration in the generate_map_link case can leak to other switch clauses. Static analysis correctly flags this issue.

Proposed fix
          case 'generate_map_link':
-            // Generate a static map link or similar
-            // We can return a result object with the link
-            const link = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/${encodeURIComponent(text)}/600x400?access_token=${process.env.MAPBOX_ACCESS_TOKEN}`;
-            result = {
-                url: link,
-                description: "Static map link generated"
-            };
-            break;
+          case 'generate_map_link': {
+            const link = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/${encodeURIComponent(text)}/600x400?access_token=${process.env.MAPBOX_ACCESS_TOKEN}`;
+            result = {
+              url: link,
+              description: 'Static map link generated',
+            };
+            break;
+          }
🧰 Tools
🪛 Biome (2.1.2)

[error] 384-386: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In @lib/agents/map-control-orchestrator.tsx around lines 381 - 389, The
'generate_map_link' switch case declares a const link which can leak into other
cases; wrap the case body in its own block scope (add { ... } after case
'generate_map_link': and before break) so the const link is scoped locally and
cannot conflict with other switch clauses — update the case block that builds
the Mapbox URL and assigns result accordingly (symbols: case
'generate_map_link', const link, result).

Comment on lines +423 to +425
// Cast result to any to avoid "Property 'data' does not exist" on generic unknown
const resAny = result as any;
const data = resAny.data || resAny;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Type assertion to any obscures Composio response structure.

Casting the result to any suppresses type checking. Consider defining a proper type for Composio tool execution results or using a narrower type assertion.

🤖 Prompt for AI Agents
In @lib/agents/map-control-orchestrator.tsx around lines 423 - 425, The current
cast of result to any (resAny) hides the real Composio tool response shape;
define a proper type/interface (e.g., ComposioExecutionResult) that models the
expected fields (like data, error, etc.), replace the any assertion with a
narrow cast to that type, and extract data via const data = (result as
ComposioExecutionResult).data ?? result to preserve compatibility; update any
related helpers/types and adjust consumers of data accordingly so type-checking
enforces the Composio response structure instead of using resAny.

Comment on lines +49 to +66
if (geom.type === 'Point') {
const [lon, lat] = geom.coordinates as [number, number];
if (Math.abs(lon) > 180 || Math.abs(lat) > 90) {
errors.push({
field: `features[${idx}].geometry.coordinates`,
message: `Invalid coordinates: [${lon}, ${lat}]. Longitude must be -180 to 180, latitude must be -90 to 90`,
severity: 'error',
});
}
} else if (geom.type === 'LineString') {
const coords = geom.coordinates as [number, number][];
if (coords.length < 2) {
errors.push({
field: `features[${idx}].geometry.coordinates`,
message: 'LineString must have at least 2 coordinates',
severity: 'error',
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Coordinate and length checks partially duplicate Zod schema validation.

The Zod schemas already enforce coordinate ranges (-180 to 180, -90 to 90) and minimum coordinate counts (LineString ≥2, Polygon ring ≥4). These semantic checks will never trigger for data that passed Zod parsing.

Consider removing redundant checks or clarifying they're intentional defense-in-depth. The duplicate ID check and ring closure check add genuine value beyond schema validation.

Comment on lines +187 to +192
// Check for reasonable values
if (cmd.params.zoom !== undefined) {
if (cmd.params.zoom < 0 || cmd.params.zoom > 22) {
warnings.push(`Unusual zoom level: ${cmd.params.zoom}. Typical range is 0-22`);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unreachable warning: Zod schema already enforces zoom range.

The MapCommandParamsSchema defines zoom: z.number().min(0).max(22), so any zoom value outside this range would cause Zod parsing to fail before reaching this check. This warning can never be triggered.

Suggested fix: remove unreachable check or adjust message
-    // Check for reasonable values
-    if (cmd.params.zoom !== undefined) {
-      if (cmd.params.zoom < 0 || cmd.params.zoom > 22) {
-        warnings.push(`Unusual zoom level: ${cmd.params.zoom}. Typical range is 0-22`);
-      }
-    }
+    // Zoom range is already validated by schema (0-22)
📝 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.

Suggested change
// Check for reasonable values
if (cmd.params.zoom !== undefined) {
if (cmd.params.zoom < 0 || cmd.params.zoom > 22) {
warnings.push(`Unusual zoom level: ${cmd.params.zoom}. Typical range is 0-22`);
}
}
// Zoom range is already validated by schema (0-22)
🤖 Prompt for AI Agents
In @lib/agents/map-workers/validator.tsx around lines 187 - 192, The zoom-range
check is unreachable because MapCommandParamsSchema enforces zoom via
z.number().min(0).max(22); remove the conditional block that inspects
cmd.params.zoom and pushes the `Unusual zoom level` warning (the if
(cmd.params.zoom !== undefined) { if (cmd.params.zoom < 0 || cmd.params.zoom >
22) { warnings.push(...) } } block) or replace it with a different runtime check
if you intend to warn about other conditions; ensure no other code expects that
warning to be produced when relying on Zod validation.

Comment on lines +98 to +99
pitch: z.number().min(0).max(85).optional(),
bearing: z.number().min(0).max(360).optional(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Bearing range may reject valid negative values.

Mapbox accepts bearing values in the range -180 to 360 (or normalized equivalents). The current constraint z.number().min(0).max(360) will reject negative bearings like -45, which are valid in Mapbox.

Proposed fix
-  bearing: z.number().min(0).max(360).optional(),
+  bearing: z.number().min(-180).max(360).optional(),
📝 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.

Suggested change
pitch: z.number().min(0).max(85).optional(),
bearing: z.number().min(0).max(360).optional(),
pitch: z.number().min(0).max(85).optional(),
bearing: z.number().min(-180).max(360).optional(),
🤖 Prompt for AI Agents
In @lib/types/map-schemas.ts around lines 98 - 99, The bearing schema currently
uses z.number().min(0).max(360).optional(), which wrongly rejects valid negative
bearings; update the bearing validator in map-schemas (the bearing field next to
pitch) to allow the full Mapbox range by changing its bounds to min(-180) and
max(360) (keeping .optional()), so negative bearings like -45 are accepted.

Comment on lines +127 to +141
// Map State Feedback Schema
export const MapStateFeedbackSchema = z.object({
success: z.boolean(),
currentBounds: z.tuple([
z.tuple([z.number(), z.number()]),
z.tuple([z.number(), z.number()]),
]).optional(),
currentCenter: z.tuple([z.number(), z.number()]).optional(),
currentZoom: z.number().optional(),
currentPitch: z.number().optional(),
currentBearing: z.number().optional(),
visibleFeatures: z.array(z.string()).optional(),
error: z.string().optional(),
timestamp: z.number(),
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding coordinate range validation for consistency.

Unlike other schemas, MapStateFeedbackSchema omits coordinate range validation for currentBounds and currentCenter. While feedback originates from the map (which should always have valid coordinates), adding validation would provide consistency with other schemas and catch unexpected data issues.

🤖 Prompt for AI Agents
In @lib/types/map-schemas.ts around lines 127 - 141, MapStateFeedbackSchema
currently accepts raw numbers for currentBounds and currentCenter; add explicit
latitude/longitude range validation to match other schemas by replacing
z.number() inside the coordinate tuples with z.number().min(-180).max(180) for
longitude and z.number().min(-90).max(90) for latitude (or the project’s chosen
order) so both currentCenter (z.tuple([lon, lat])) and each coordinate in
currentBounds’ nested tuples are validated; update MapStateFeedbackSchema to use
those bounded z.number() validators for all coordinate entries.

@ngoiyaeric
Copy link
Collaborator Author

@coderabbitai GeoJSON Parser error: Error [AI_APICallError]: Invalid arguments passed to the model.
at (.next/server/chunks/3738.js:5:5155)
at async tn (.next/server/chunks/3738.js:5:4010)
at async ty.doGenerate (.next/server/chunks/3738.js:5:11179)
at async fn (.next/server/chunks/5641.js:116:10748)
at async (.next/server/chunks/5641.js:112:6618)
at async lX (.next/server/chunks/5641.js:112:4433)
at async fn (.next/server/chunks/5641.js:116:10135)
at async (.next/server/chunks/5641.js:112:6618)
at async j (.next/server/chunks/5202.js:244:64) {
cause: undefined,
url: 'https://api.x.ai/v1/chat/completions',
requestBodyValues: [Object],
statusCode: 400,
responseHeaders: [Object],
responseBody: '{"code":"Client specified an invalid argument","error":"Invalid arguments passed to the model."}',
isRetryable: false,
data: [Object]
}

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 13, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== 1. Check GeoJSON Parser implementation ==="
cat lib/agents/map-workers/geojson-parser.tsx

echo -e "\n=== 2. Check what getModel() returns ==="
cat lib/utils.ts | grep -A 30 "export.*getModel"

echo -e "\n=== 3. Check GeoJSONParserOutputSchema ==="
cat lib/types/map-schemas.ts | grep -A 30 "GeoJSONParserOutputSchema"

echo -e "\n=== 4. Check if X.AI is being used ==="
rg -n "x-ai|x\.ai|grok" .env.example .env.local lib/utils.ts --no-ignore 2>/dev/null || echo "No X.AI references found in common config files"

echo -e "\n=== 5. Check GEOJSON_PARSER_PROMPT length ==="
cat lib/agents/map-workers/geojson-parser.tsx | grep -A 100 "GEOJSON_PARSER_PROMPT" | wc -l

echo -e "\n=== 6. Check if generateObject is being called correctly ==="
rg -n "generateObject" lib/agents/map-workers/geojson-parser.tsx -A 5 -B 5

Length of output: 5849


🏁 Script executed:

#!/bin/bash

echo "=== 7. Find getModel definition ==="
rg -n "export.*function getModel|export.*const getModel" --type ts --type tsx -A 20

echo -e "\n=== 8. Check environment variables for AI provider ==="
cat .env.example | grep -i "model\|provider\|api"

echo -e "\n=== 9. Check if X.AI supports structured output ==="
rg -n "structuredOutputs|tool_choice|response_format" lib/ --type ts --type tsx | head -20

echo -e "\n=== 10. Check AI SDK configuration ==="
rg -n "from 'ai'" lib/agents/map-workers/ -B 1 -A 1

Length of output: 806


🏁 Script executed:

#!/bin/bash

echo "=== 11. Find getModel with broader search ==="
fd -e ts -e tsx -e js -e jsx "utils" lib/ --exec grep -l "getModel" {} \; | head -5

echo -e "\n=== 12. Search for getModel in any file ==="
rg -n "function getModel|const getModel" -A 15

echo -e "\n=== 13. Check package.json for AI SDK and providers ==="
cat package.json | jq '.dependencies | with_entries(select(.key | contains("ai") or contains("openai") or contains("anthropic") or contains("xai")))'

echo -e "\n=== 14. Check for model registry or config ==="
rg -n "createOpenAI|createAnthropic|createXAI|registry" lib/ app/ -A 3 | head -40

echo -e "\n=== 15. Check what models are available ==="
rg -n "grok|x\.ai|xai|gpt|claude" lib/ app/ --type-add 'code:*.{ts,tsx,js,jsx}' --type code | head -30

Length of output: 3339


🏁 Script executed:

#!/bin/bash

echo "=== 16. Check X.AI model configuration in detail ==="
cat lib/utils/index.ts | grep -A 50 "case 'Grok 4.2'"

echo -e "\n=== 17. Check AI SDK version for X.AI support ==="
cat package.json | jq '.dependencies["@ai-sdk/xai"], .dependencies["ai"]'

echo -e "\n=== 18. Look for any X.AI structured output handling ==="
rg -n "structuredOutputs|mode.*json|response_format" lib/utils/index.ts -A 3

echo -e "\n=== 19. Check how other workers use generateObject ==="
cat lib/agents/map-workers/map-command-generator.tsx | grep -A 10 "generateObject"

Length of output: 2893


@ngoiyaeric Thanks for the error dump — I can reproduce the root cause from the code you shared.

Root cause

  • geojsonParser (and other workers) call ai.generateObject with a JSON schema. getModel() currently resolves to X.AI grok-4-fast-non-reasoning (because XAI_API_KEY is set and/or selectedModel='Grok 4.2').
  • X.AI’s chat/completions endpoint rejects the structured-output arguments the AI SDK sends for generateObject, returning 400 "Invalid arguments passed to the model."
  • So, any worker using generateObject will fail under X.AI. This matches your stack trace.

Immediate mitigation you can apply on Vercel (no code change)

  • Set OPENAI_API_KEY and force a model that supports structured outputs:
    • Add env vars: OPENAI_API_KEY=, SELECTED_MODEL=GPT-5.1
    • Remove XAI_API_KEY (or leave it unset), or set SELECTED_MODEL to GPT-5.1 so getModel returns gpt-4o via @ai-sdk/openai.
  • Redeploy. This will route generateObject to OpenAI and unblock the parser immediately.

Robust code fixes (recommended)

  1. Add a structured-output-aware model selector
  • lib/utils/index.ts
    • Export a new helper that guarantees a provider compatible with generateObject (OpenAI preferred; fallback to Bedrock Anthropic if you’ve wired it for structured outputs; otherwise throw with a clear error).

TypeScript patch:

// lib/utils/index.ts
import { createOpenAI } from '@ai-sdk/openai'
import { createXai } from '@ai-sdk/xai'
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'
// ...existing imports...

export async function getStructuredModel(requireVision: boolean = false) {
  const openaiApiKey = process.env.OPENAI_API_KEY;
  const bedrockModelId = process.env.BEDROCK_MODEL_ID || 'anthropic.claude-3-5-sonnet-20241022-v2:0';
  const xaiApiKey = process.env.XAI_API_KEY;

  // Prefer OpenAI for structured outputs
  if (openaiApiKey) {
    const openai = createOpenAI({ apiKey: openaiApiKey });
    // gpt-4o supports structured outputs in AI SDK
    return openai('gpt-4o');
  }

  // Optional: fallback to Bedrock Anthropic if you’ve validated structured outputs in your setup
  // const bedrock = createAmazonBedrock({ aws: { accessKeyId, secretAccessKey, region } });
  // return bedrock(bedrockModelId);

  // Explicitly avoid X.AI here for structured outputs
  if (xaiApiKey) {
    console.warn('[getStructuredModel] X.AI detected but skipped for structured outputs. Set OPENAI_API_KEY to enable structured outputs.');
  }
  throw new Error('No provider configured for structured outputs. Set OPENAI_API_KEY to use OpenAI for generateObject().');
}
  1. Use getStructuredModel() in workers that call generateObject
  • lib/agents/map-workers/geojson-parser.tsx
  • lib/agents/map-workers/map-command-generator.tsx
  • lib/agents/map-workers/feedback-analyzer.tsx

Patch example (geojson-parser.tsx):

// lib/agents/map-workers/geojson-parser.tsx
import { generateObject } from 'ai';
- import { getModel } from '@/lib/utils';
+ import { getStructuredModel } from '@/lib/utils';

export async function geojsonParser(text: string) {
-  const model = await getModel();
+  const model = await getStructuredModel(); // ensures provider supports JSON schema
  const { object } = await generateObject({
    model,
    schema: GeoJSONParserOutputSchema,
    prompt: `${GEOJSON_PARSER_PROMPT}\n\nText to analyze:\n${text}`,
    maxTokens: 2048,
  });
  return object;
}

Apply the same import/substitution in:

  • lib/agents/map-workers/map-command-generator.tsx
  • lib/agents/map-workers/feedback-analyzer.tsx
  1. Optional fallback to text mode if no structured provider is available
    If you want maximum resilience, wrap generateObject in a try/catch and fall back to a plain text JSON extraction:
try {
  const { object } = await generateObject({ model, schema, prompt, maxTokens: 2048 });
  return object;
} catch (err) {
  console.warn('[geojsonParser] structured output failed, falling back to text JSON parse:', err);
  const { text } = await model.generateText({ prompt: `${GEOJSON_PARSER_PROMPT}\n\nText to analyze:\n${text}\n\nOutput ONLY valid JSON matching the schema.` });
  // safe-parse with Zod
  const parsed = GeoJSONParserOutputSchema.safeParse(JSON.parse(text));
  if (parsed.success) return parsed.data;
  return { geojson: null, confidence: 0, extractedLocations: [], warnings: ['Parser fallback failed'] };
}
  1. Fix the separate build error in app/actions.tsx
    You’re passing a nullable client where undefined is expected:
  • Change the orchestrator call to pass undefined if the Composio client init fails.
// app/actions.tsx (around the orchestrator invocation)
const init = await initializeComposioMapbox().catch(() => null);
const mcpClient = init?.composio ?? undefined; // ensure undefined, not null
locationResponse = await mapControlOrchestrator(answer, {
  mcpClient,               // now type-safe
  connectionId,
  enableFeedbackLoop: false,
});

Sanity checks after change

  • Log which provider/model is selected for structured outputs:
console.log('[getStructuredModel] Using OpenAI gpt-4o for structured outputs');
  • Validate end-to-end by issuing a simple location query:
    • “Show me Paris”
    • Expect geojsonParser to return a FeatureCollection or extractedLocations=['Paris'] with confidence > 0.

If you’d like, I can prepare a PR-ready patch set for the three workers and utils, and add a short README blurb explaining that structured extractions are routed to OpenAI. Let me know and I’ll stage the plan.

[plan_code_changes]

  • Add getStructuredModel() in lib/utils/index.ts to force a provider compatible with ai.generateObject (OpenAI gpt-4o).
  • Update geojsonParser, map-command-generator, and feedback-analyzer to use getStructuredModel().
  • Add safe fallback to text+Zod parsing when structured outputs are unavailable.
  • Fix null→undefined in app/actions.tsx orchestrator call.

✏️ Learnings added
Learnt from: ngoiyaeric
URL: https://github.com/QueueLab/QCX/pull/312

Timestamp: 2026-01-13T13:26:30.076Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In @lib/agents/map-workers/feedback-analyzer.tsx:
- Around line 50-65: The feedbackAnalyzer currently calls getModel() and passes
that model to generateObject, but getModel() can return X.AI grok models that
reject JSON Schema structured-output args; change feedbackAnalyzer to obtain a
structured-output-compatible model instead (e.g., implement or call
getStructuredModel()) and pass that to generateObject so schema-based generation
won't hit the error path; you can implement getStructuredModel to prefer OpenAI
(using createOpenAI + 'gpt-4o' when OPENAI_API_KEY is present) and fall back to
getModel() otherwise, then replace the model = await getModel() line in
feedbackAnalyzer with model = await getStructuredModel().
- Around line 155-163: postProcessAnalysis is mutating its input
FeedbackAnalysis object; instead return a new FeedbackAnalysis without side
effects: inside postProcessAnalysis, when attemptNumber >= 3 create and return a
shallow copy of the original (spread the top-level FeedbackAnalysis) and also
copy-and-replace the nested recommendations (spread object.recommendations and
set action='abort' and reasoning='Maximum retry attempts reached (3)...');
otherwise return a shallow copy of the original to guarantee no mutation. Ensure
you update references to FeedbackAnalysis, postProcessAnalysis,
recommendations.action and recommendations.reasoning accordingly.

In @lib/agents/map-workers/geojson-parser.tsx:
- Around line 1-6: The file geojson-parser.tsx contains no JSX and should be
renamed to geojson-parser.ts; rename the file, update any imports that reference
"geojson-parser.tsx" to "geojson-parser.ts" (search for imports of
geojson-parser), and ensure your build/TS config (tsconfig include/paths and any
ESLint rules) accepts the .ts filename; after renaming, run typecheck and the
test/build to verify no import paths or tooling assumptions remain pointing to
the .tsx name.
- Around line 72-85: geojsonParser currently calls getModel() which can return
X.AI Grok models that reject JSON Schema structured-output args and cause
generateObject to fail; introduce a getStructuredModel() helper that prefers an
OpenAI model for structured outputs (falling back to getModel()), then update
geojsonParser to call getStructuredModel() instead of getModel() before calling
generateObject so generateObject receives a model that supports structured
outputs; reference getStructuredModel, geojsonParser, generateObject, and
getModel when making the change.
- Around line 109-118: The returned warnings lose the fallback error context
when both generateObject and the fallback fail; update the error handling in the
catch/fallback block (referencing generateObject, fallbackError, and the place
that returns { geojson, confidence, extractedLocations, warnings }) to include
both the original error and the fallbackError (e.g., combine messages and/or
stacks into the warnings array), and ensure the processLogger/console.error
still logs the fallbackError while the returned warnings contain clear context
for both failures.

In @lib/agents/map-workers/map-command-generator.tsx:
- Around line 1-7: This file contains no JSX so rename map-command-generator.tsx
to map-command-generator.ts to avoid TSX parsing overhead, update any imports
that reference this module accordingly (search for references to
map-command-generator.tsx and change to .ts), verify exported symbols like
generateObject/generateText/getModel and types
MapCommandGeneratorOutputSchema/MapCommandGeneratorOutput/GeoJSONFeatureCollection
remain unchanged and that the file has no JSX; run TypeScript build/IDE to
ensure no broken imports after the rename.
- Around line 73-88: The current mapCommandGenerator calls getModel() then uses
generateObject with MapCommandGeneratorOutputSchema, but getModel() can return
X.AI Grok which rejects JSON Schema structured-output; change
mapCommandGenerator to use a model that supports structured outputs by either
(a) implementing and calling a new getStructuredModel() helper that prefers
OpenAI gpt-4o (or other structured-output-capable models) instead of getModel(),
or (b) modifying getModel() to accept a flag (e.g., { structured: true }) and
pass that flag from mapCommandGenerator; ensure the call site referencing
generateObject still passes model, schema: MapCommandGeneratorOutputSchema,
prompt, maxTokens, and that any fallback logic to generateText is only used if
generateObject errors after using the structured-capable model.
- Around line 89-136: When the text fallback JSON is parsed, the Zod validation
result (MapCommandGeneratorOutputSchema.safeParse) is ignored on failure; add
logging inside the generateObject catch fallback: after const parsed =
MapCommandGeneratorOutputSchema.safeParse(object) check the parsed.success
branch and, in the else branch, log the parsed.error (or parsed) with context
(e.g., "Map Command Generator: Zod validation failed for text fallback") before
continuing to the GeoJSON and empty-command fallbacks; ensure logging references
the generateText response (jsonText) and still returns
postProcessOutput(parsed.data) when parsed.success is true.
- Around line 160-180: postProcessOutput currently mutates its input object
(object.commands and object.estimatedDuration); make it pure by returning a new
MapCommandGeneratorOutput instead of modifying the input: in postProcessOutput
create a shallow copy of the input (e.g., newObject = { ...object }) and
copy/replace the commands array with a sorted copy (e.g., newCommands =
[...object.commands].sort(...)) and compute estimatedDuration on the newObject
without altering the original; ensure you return newObject and leave the
original object.commands and estimatedDuration unchanged.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ea8401 and 842c51e.

📒 Files selected for processing (3)
  • lib/agents/map-workers/feedback-analyzer.tsx
  • lib/agents/map-workers/geojson-parser.tsx
  • lib/agents/map-workers/map-command-generator.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-13T13:26:30.076Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.
📚 Learning: 2026-01-13T13:26:30.076Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-13T13:26:30.076Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.

Applied to files:

  • lib/agents/map-workers/geojson-parser.tsx
  • lib/agents/map-workers/map-command-generator.tsx
🧬 Code graph analysis (3)
lib/agents/map-workers/geojson-parser.tsx (2)
lib/types/map-schemas.ts (2)
  • GeoJSONParserOutput (227-227)
  • GeoJSONParserOutputSchema (155-160)
lib/utils/index.ts (1)
  • getModel (20-121)
lib/agents/map-workers/feedback-analyzer.tsx (2)
lib/types/map-schemas.ts (4)
  • MapStateFeedback (225-225)
  • MapCommand (223-223)
  • FeedbackAnalysis (226-226)
  • FeedbackAnalysisSchema (144-152)
lib/utils/index.ts (1)
  • getModel (20-121)
lib/agents/map-workers/map-command-generator.tsx (2)
lib/types/map-schemas.ts (3)
  • GeoJSONFeatureCollection (222-222)
  • MapCommandGeneratorOutput (228-228)
  • MapCommandGeneratorOutputSchema (162-166)
lib/utils/index.ts (1)
  • getModel (20-121)
🔇 Additional comments (9)
lib/agents/map-workers/feedback-analyzer.tsx (5)

1-33: LGTM on imports and prompt definition.

The prompt is well-structured with clear role definition, analysis criteria, action definitions, and expected output format.


35-44: LGTM on interface definition.

The FeedbackAnalyzerInput interface is well-typed and aligns with the schema types from map-schemas.ts.


66-112: Well-implemented fallback chain.

The three-tier fallback (generateObject → generateText with JSON parsing → deterministic analysis) provides robust error handling. The markdown code block stripping on line 76 and Zod validation ensures safe parsing of LLM text output.


115-153: LGTM on context builder.

The buildContextString function produces a well-structured context with all relevant feedback data for the LLM to analyze.


165-253: LGTM on helper functions and synchronous fallback.

The coordinatesMatch and zoomMatches helpers use sensible default tolerances (0.0001 for coordinates, 0.5 for zoom). The simpleFeedbackAnalysis function provides a reliable deterministic fallback with clear status and action determination logic.

lib/agents/map-workers/geojson-parser.tsx (1)

8-66: LGTM!

The prompt is well-structured with clear rules, coordinate validation requirements, and good examples covering all confidence levels.

lib/agents/map-workers/map-command-generator.tsx (3)

9-57: Well-structured prompt with comprehensive command documentation.

The prompt provides clear guidance on command selection, zoom levels, and camera parameters. The available commands (flyTo, easeTo, fitBounds, setCenter, setZoom) are well-documented with use cases.

Note: Per PR objectives, consider adding standalone setPitch and setBearing commands if the Mapbox component supports them independently, though pitch/bearing are already covered as parameters within existing commands.


59-67: Interface is well-defined for map command generation context.

The input structure appropriately captures text intent, geographic data, and optional current map state for context-aware command generation.


139-158: Context builder is functional and provides sufficient information for command generation.

The function appropriately includes text, feature metadata, and current map state. One consideration: including feature coordinates in the context string could help the model make better bounding box decisions for fitBounds, but the current approach is reasonable since the model receives the full GeoJSON schema context.

Comment on lines +50 to +65
export async function feedbackAnalyzer(
input: FeedbackAnalyzerInput
): Promise<FeedbackAnalysis> {
const model = await getModel();

const prompt = `${FEEDBACK_ANALYZER_PROMPT}\n\n${buildContextString(input)}\n\nAnalyze this feedback and provide recommendations.`;

try {
const { object } = await generateObject({
model,
schema: FeedbackAnalysisSchema,
prompt,
maxTokens: 1024,
});

return postProcessAnalysis(object, input.attemptNumber);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a structured-output-compatible model for generateObject.

Based on learnings, getModel() can return X.AI grok-4-fast-non-reasoning which rejects JSON Schema structured-output arguments, causing generateObject to fail. While the fallback chain handles this gracefully, it's inefficient to always trigger the error path when XAI_API_KEY is set.

Consider implementing or using a getStructuredModel() helper that prefers OpenAI gpt-4o for structured outputs, as mentioned in the PR objectives.

🔧 Suggested approach
-  const model = await getModel();
+  const model = await getStructuredModel(); // Prefers OpenAI for structured outputs

Or inline the OpenAI preference:

import { createOpenAI } from '@ai-sdk/openai';

async function getStructuredModel() {
  const openaiApiKey = process.env.OPENAI_API_KEY;
  if (openaiApiKey) {
    const openai = createOpenAI({ apiKey: openaiApiKey });
    return openai('gpt-4o');
  }
  // Fall back to default model for text generation fallback
  return getModel();
}
🤖 Prompt for AI Agents
In @lib/agents/map-workers/feedback-analyzer.tsx around lines 50 - 65, The
feedbackAnalyzer currently calls getModel() and passes that model to
generateObject, but getModel() can return X.AI grok models that reject JSON
Schema structured-output args; change feedbackAnalyzer to obtain a
structured-output-compatible model instead (e.g., implement or call
getStructuredModel()) and pass that to generateObject so schema-based generation
won't hit the error path; you can implement getStructuredModel to prefer OpenAI
(using createOpenAI + 'gpt-4o' when OPENAI_API_KEY is present) and fall back to
getModel() otherwise, then replace the model = await getModel() line in
feedbackAnalyzer with model = await getStructuredModel().

Comment on lines +155 to +163
function postProcessAnalysis(object: FeedbackAnalysis, attemptNumber: number): FeedbackAnalysis {
// Apply attempt-based logic
if (attemptNumber >= 3 && object.recommendations.action !== 'continue') {
// After 3 attempts, force abort if not successful
object.recommendations.action = 'abort';
object.recommendations.reasoning = 'Maximum retry attempts reached (3). Aborting to prevent infinite loop.';
}
return object;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid mutating the input parameter.

postProcessAnalysis directly mutates the object parameter (lines 159-160). This can cause unexpected side effects if the caller retains a reference to the original object.

♻️ Suggested fix using spread operator
 function postProcessAnalysis(object: FeedbackAnalysis, attemptNumber: number): FeedbackAnalysis {
     // Apply attempt-based logic
     if (attemptNumber >= 3 && object.recommendations.action !== 'continue') {
       // After 3 attempts, force abort if not successful
-      object.recommendations.action = 'abort';
-      object.recommendations.reasoning = 'Maximum retry attempts reached (3). Aborting to prevent infinite loop.';
+      return {
+        ...object,
+        recommendations: {
+          ...object.recommendations,
+          action: 'abort',
+          reasoning: 'Maximum retry attempts reached (3). Aborting to prevent infinite loop.',
+        },
+      };
     }
     return object;
 }
📝 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.

Suggested change
function postProcessAnalysis(object: FeedbackAnalysis, attemptNumber: number): FeedbackAnalysis {
// Apply attempt-based logic
if (attemptNumber >= 3 && object.recommendations.action !== 'continue') {
// After 3 attempts, force abort if not successful
object.recommendations.action = 'abort';
object.recommendations.reasoning = 'Maximum retry attempts reached (3). Aborting to prevent infinite loop.';
}
return object;
}
function postProcessAnalysis(object: FeedbackAnalysis, attemptNumber: number): FeedbackAnalysis {
// Apply attempt-based logic
if (attemptNumber >= 3 && object.recommendations.action !== 'continue') {
// After 3 attempts, force abort if not successful
return {
...object,
recommendations: {
...object.recommendations,
action: 'abort',
reasoning: 'Maximum retry attempts reached (3). Aborting to prevent infinite loop.',
},
};
}
return object;
}
🤖 Prompt for AI Agents
In @lib/agents/map-workers/feedback-analyzer.tsx around lines 155 - 163,
postProcessAnalysis is mutating its input FeedbackAnalysis object; instead
return a new FeedbackAnalysis without side effects: inside postProcessAnalysis,
when attemptNumber >= 3 create and return a shallow copy of the original (spread
the top-level FeedbackAnalysis) and also copy-and-replace the nested
recommendations (spread object.recommendations and set action='abort' and
reasoning='Maximum retry attempts reached (3)...'); otherwise return a shallow
copy of the original to guarantee no mutation. Ensure you update references to
FeedbackAnalysis, postProcessAnalysis, recommendations.action and
recommendations.reasoning accordingly.

Comment on lines +1 to +6
import { generateObject, generateText } from 'ai';
import { getModel } from '@/lib/utils';
import {
GeoJSONParserOutputSchema,
GeoJSONParserOutput,
} from '@/lib/types/map-schemas';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider renaming file to .ts extension.

This file contains no JSX; using .tsx is unnecessary and may confuse tooling or other developers.

🤖 Prompt for AI Agents
In @lib/agents/map-workers/geojson-parser.tsx around lines 1 - 6, The file
geojson-parser.tsx contains no JSX and should be renamed to geojson-parser.ts;
rename the file, update any imports that reference "geojson-parser.tsx" to
"geojson-parser.ts" (search for imports of geojson-parser), and ensure your
build/TS config (tsconfig include/paths and any ESLint rules) accepts the .ts
filename; after renaming, run typecheck and the test/build to verify no import
paths or tooling assumptions remain pointing to the .tsx name.

Comment on lines +72 to +85
export async function geojsonParser(
text: string
): Promise<GeoJSONParserOutput> {
const model = await getModel();

try {
const { object } = await generateObject({
model,
schema: GeoJSONParserOutputSchema,
prompt: `${GEOJSON_PARSER_PROMPT}\n\nText to analyze:\n${text}`,
maxTokens: 2048,
});

return validateAndReturn(object);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: getModel() may return X.AI Grok which does not support structured outputs.

Based on learnings, X.AI's grok-4-fast-non-reasoning model rejects JSON Schema structured-output arguments with a 400 error. The getModel() call can return this model, causing generateObject to always fail and fall back to generateText, adding latency and log noise.

Introduce a getStructuredModel() helper that prefers OpenAI for structured outputs, or pass a model override parameter.

🔧 Suggested fix
-import { getModel } from '@/lib/utils';
+import { getModel, getStructuredModel } from '@/lib/utils';
 export async function geojsonParser(
   text: string
 ): Promise<GeoJSONParserOutput> {
-  const model = await getModel();
+  // Use structured-output-compatible model (OpenAI preferred)
+  const structuredModel = await getStructuredModel();
+  const fallbackModel = await getModel();

   try {
     const { object } = await generateObject({
-      model,
+      model: structuredModel,
       schema: GeoJSONParserOutputSchema,

Then in lib/utils/index.ts, add:

export async function getStructuredModel() {
  // Prefer OpenAI for structured outputs as X.AI Grok rejects JSON Schema
  if (process.env.OPENAI_API_KEY) {
    const { openai } = await import('@ai-sdk/openai');
    return openai('gpt-4o');
  }
  // Fallback to default model (may not support structured outputs)
  return getModel();
}
🤖 Prompt for AI Agents
In @lib/agents/map-workers/geojson-parser.tsx around lines 72 - 85,
geojsonParser currently calls getModel() which can return X.AI Grok models that
reject JSON Schema structured-output args and cause generateObject to fail;
introduce a getStructuredModel() helper that prefers an OpenAI model for
structured outputs (falling back to getModel()), then update geojsonParser to
call getStructuredModel() instead of getModel() before calling generateObject so
generateObject receives a model that supports structured outputs; reference
getStructuredModel, geojsonParser, generateObject, and getModel when making the
change.

Comment on lines +109 to +118
} catch (fallbackError) {
console.error('GeoJSON Parser: Fallback failed:', fallbackError);
}

return {
geojson: null,
confidence: 0,
extractedLocations: [],
warnings: [`Parser error: ${error instanceof Error ? error.message : 'Unknown error'}`],
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error message loses context when fallback also fails.

When both generateObject and the fallback fail, the returned warning only includes the original error. If the fallback fails for a different reason (e.g., JSON parse error), that context is lost.

💡 Suggested fix
     } catch (fallbackError) {
       console.error('GeoJSON Parser: Fallback failed:', fallbackError);
+      return {
+        geojson: null,
+        confidence: 0,
+        extractedLocations: [],
+        warnings: [
+          `Primary error: ${error instanceof Error ? error.message : 'Unknown'}`,
+          `Fallback error: ${fallbackError instanceof Error ? fallbackError.message : 'Unknown'}`,
+        ],
+      };
     }

     return {
       geojson: null,
       confidence: 0,
       extractedLocations: [],
-      warnings: [`Parser error: ${error instanceof Error ? error.message : 'Unknown error'}`],
+      warnings: [`Parser error: ${error instanceof Error ? error.message : 'Unknown error'} (fallback also failed)`],
     };
🤖 Prompt for AI Agents
In @lib/agents/map-workers/geojson-parser.tsx around lines 109 - 118, The
returned warnings lose the fallback error context when both generateObject and
the fallback fail; update the error handling in the catch/fallback block
(referencing generateObject, fallbackError, and the place that returns {
geojson, confidence, extractedLocations, warnings }) to include both the
original error and the fallbackError (e.g., combine messages and/or stacks into
the warnings array), and ensure the processLogger/console.error still logs the
fallbackError while the returned warnings contain clear context for both
failures.

Comment on lines +122 to +138
function validateAndReturn(object: GeoJSONParserOutput): GeoJSONParserOutput {
if (object.geojson) {
// Validate coordinate ranges
for (const feature of object.geojson.features) {
const coords = feature.geometry.coordinates;
if (feature.geometry.type === 'Point') {
const [lon, lat] = coords as [number, number];
if (lon < -180 || lon > 180 || lat < -90 || lat > 90) {
object.warnings = object.warnings || [];
object.warnings.push(`Invalid coordinates for feature: [${lon}, ${lat}]`);
object.confidence = Math.min(object.confidence, 0.5);
}
}
}
}
return object;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incomplete coordinate validation: LineString and Polygon geometries are not validated.

The prompt specifies that LineString and Polygon geometries are supported, but validateAndReturn only validates Point coordinates. Invalid coordinates in routes or areas will pass through undetected.

Additionally, this function mutates the input object directly. Consider either cloning the object or documenting this side effect.

🔧 Suggested fix with full geometry validation
 function validateAndReturn(object: GeoJSONParserOutput): GeoJSONParserOutput {
+    const result = { ...object, warnings: [...(object.warnings || [])] };
+    
-    if (object.geojson) {
-      // Validate coordinate ranges
-      for (const feature of object.geojson.features) {
+    if (result.geojson) {
+      for (const feature of result.geojson.features) {
         const coords = feature.geometry.coordinates;
+        const invalidCoords: string[] = [];
+        
+        const validatePoint = (lon: number, lat: number): boolean => {
+          if (lon < -180 || lon > 180 || lat < -90 || lat > 90) {
+            invalidCoords.push(`[${lon}, ${lat}]`);
+            return false;
+          }
+          return true;
+        };
+        
         if (feature.geometry.type === 'Point') {
           const [lon, lat] = coords as [number, number];
-          if (lon < -180 || lon > 180 || lat < -90 || lat > 90) {
-            object.warnings = object.warnings || [];
-            object.warnings.push(`Invalid coordinates for feature: [${lon}, ${lat}]`);
-            object.confidence = Math.min(object.confidence, 0.5);
-          }
+          validatePoint(lon, lat);
+        } else if (feature.geometry.type === 'LineString') {
+          for (const point of coords as [number, number][]) {
+            validatePoint(point[0], point[1]);
+          }
+        } else if (feature.geometry.type === 'Polygon') {
+          for (const ring of coords as [number, number][][]) {
+            for (const point of ring) {
+              validatePoint(point[0], point[1]);
+            }
+          }
+        }
+        
+        if (invalidCoords.length > 0) {
+          result.warnings.push(`Invalid coordinates in ${feature.geometry.type}: ${invalidCoords.join(', ')}`);
+          result.confidence = Math.min(result.confidence, 0.5);
         }
       }
     }
-    return object;
+    return result;
 }

Comment on lines +1 to +7
import { generateObject, generateText } from 'ai';
import { getModel } from '@/lib/utils';
import {
MapCommandGeneratorOutputSchema,
MapCommandGeneratorOutput,
GeoJSONFeatureCollection,
} from '@/lib/types/map-schemas';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider using .ts extension instead of .tsx.

This file contains no JSX elements, so a .ts extension would be more accurate and avoids unnecessary TSX parsing overhead.

🤖 Prompt for AI Agents
In @lib/agents/map-workers/map-command-generator.tsx around lines 1 - 7, This
file contains no JSX so rename map-command-generator.tsx to
map-command-generator.ts to avoid TSX parsing overhead, update any imports that
reference this module accordingly (search for references to
map-command-generator.tsx and change to .ts), verify exported symbols like
generateObject/generateText/getModel and types
MapCommandGeneratorOutputSchema/MapCommandGeneratorOutput/GeoJSONFeatureCollection
remain unchanged and that the file has no JSX; run TypeScript build/IDE to
ensure no broken imports after the rename.

Comment on lines +73 to +88
export async function mapCommandGenerator(
input: MapCommandGeneratorInput
): Promise<MapCommandGeneratorOutput> {
const model = await getModel();

const prompt = `${MAP_COMMAND_GENERATOR_PROMPT}\n\n${buildContextString(input)}\n\nGenerate appropriate map commands for this context.`;

try {
const { object } = await generateObject({
model,
schema: MapCommandGeneratorOutputSchema,
prompt,
maxTokens: 1536,
});

return postProcessOutput(object);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: getModel() may return X.AI Grok which doesn't support structured outputs.

Based on learnings, workers using generateObject must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI's chat/completions endpoint rejects JSON Schema structured-output arguments with a 400 error. The current getModel() defaults to Grok first (per the util snippet), causing generateObject to fail and forcing unnecessary fallback to generateText.

Consider implementing a getStructuredModel() helper that prefers OpenAI gpt-4o for structured outputs, or pass a flag to getModel() to indicate structured output requirement.

Suggested approach
-  const model = await getModel();
+  // Use a model that supports structured outputs (OpenAI preferred)
+  const model = await getStructuredModel(); // or getModel({ requireStructuredOutput: true })

The getStructuredModel() helper should prioritize OpenAI gpt-4o which reliably supports generateObject with JSON Schema.

🤖 Prompt for AI Agents
In @lib/agents/map-workers/map-command-generator.tsx around lines 73 - 88, The
current mapCommandGenerator calls getModel() then uses generateObject with
MapCommandGeneratorOutputSchema, but getModel() can return X.AI Grok which
rejects JSON Schema structured-output; change mapCommandGenerator to use a model
that supports structured outputs by either (a) implementing and calling a new
getStructuredModel() helper that prefers OpenAI gpt-4o (or other
structured-output-capable models) instead of getModel(), or (b) modifying
getModel() to accept a flag (e.g., { structured: true }) and pass that flag from
mapCommandGenerator; ensure the call site referencing generateObject still
passes model, schema: MapCommandGeneratorOutputSchema, prompt, maxTokens, and
that any fallback logic to generateText is only used if generateObject errors
after using the structured-capable model.

Comment on lines +89 to +136
} catch (error) {
console.warn('Map Command Generator: generateObject failed, attempting fallback to generateText:', error);

try {
const { text: jsonText } = await generateText({
model,
prompt: `${prompt}\n\nIMPORTANT: Return ONLY valid JSON matching the Output Structure. Do not include markdown formatting.`,
maxTokens: 1536,
});

const cleanedText = jsonText.replace(/```json\n?|\n?```/g, '').trim();
const object = JSON.parse(cleanedText);

const parsed = MapCommandGeneratorOutputSchema.safeParse(object);
if (parsed.success) {
return postProcessOutput(parsed.data);
}
} catch (fallbackError) {
console.error('Map Command Generator: Fallback failed:', fallbackError);
}

// Fallback: Generate basic command based on GeoJSON
if (input.geojson && input.geojson.features.length > 0) {
const firstFeature = input.geojson.features[0];
if (firstFeature.geometry.type === 'Point') {
const [lon, lat] = firstFeature.geometry.coordinates as [number, number];
return {
commands: [{
command: 'flyTo',
params: {
center: [lon, lat],
zoom: 12,
essential: true,
},
priority: 1,
}],
reasoning: 'Fallback: Flying to first point feature',
estimatedDuration: 2000,
};
}
}

return {
commands: [],
reasoning: `Command generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
estimatedDuration: 0,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Robust fallback chain with good error recovery.

The three-tier fallback (generateText → GeoJSON Point → empty commands) provides graceful degradation. The Zod safeParse validation on line 102 ensures schema compliance before using text-generated output.

Minor improvement: Consider logging when Zod validation fails (line 102-105) to aid debugging, as currently it falls through silently to the GeoJSON fallback.

Optional: Add logging for Zod validation failure
      const parsed = MapCommandGeneratorOutputSchema.safeParse(object);
      if (parsed.success) {
        return postProcessOutput(parsed.data);
+     } else {
+       console.warn('Map Command Generator: Zod validation failed:', parsed.error.issues);
      }
📝 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.

Suggested change
} catch (error) {
console.warn('Map Command Generator: generateObject failed, attempting fallback to generateText:', error);
try {
const { text: jsonText } = await generateText({
model,
prompt: `${prompt}\n\nIMPORTANT: Return ONLY valid JSON matching the Output Structure. Do not include markdown formatting.`,
maxTokens: 1536,
});
const cleanedText = jsonText.replace(/```json\n?|\n?```/g, '').trim();
const object = JSON.parse(cleanedText);
const parsed = MapCommandGeneratorOutputSchema.safeParse(object);
if (parsed.success) {
return postProcessOutput(parsed.data);
}
} catch (fallbackError) {
console.error('Map Command Generator: Fallback failed:', fallbackError);
}
// Fallback: Generate basic command based on GeoJSON
if (input.geojson && input.geojson.features.length > 0) {
const firstFeature = input.geojson.features[0];
if (firstFeature.geometry.type === 'Point') {
const [lon, lat] = firstFeature.geometry.coordinates as [number, number];
return {
commands: [{
command: 'flyTo',
params: {
center: [lon, lat],
zoom: 12,
essential: true,
},
priority: 1,
}],
reasoning: 'Fallback: Flying to first point feature',
estimatedDuration: 2000,
};
}
}
return {
commands: [],
reasoning: `Command generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
estimatedDuration: 0,
};
}
} catch (error) {
console.warn('Map Command Generator: generateObject failed, attempting fallback to generateText:', error);
try {
const { text: jsonText } = await generateText({
model,
prompt: `${prompt}\n\nIMPORTANT: Return ONLY valid JSON matching the Output Structure. Do not include markdown formatting.`,
maxTokens: 1536,
});
const cleanedText = jsonText.replace(/
🤖 Prompt for AI Agents
In @lib/agents/map-workers/map-command-generator.tsx around lines 89 - 136, When
the text fallback JSON is parsed, the Zod validation result
(MapCommandGeneratorOutputSchema.safeParse) is ignored on failure; add logging
inside the generateObject catch fallback: after const parsed =
MapCommandGeneratorOutputSchema.safeParse(object) check the parsed.success
branch and, in the else branch, log the parsed.error (or parsed) with context
(e.g., "Map Command Generator: Zod validation failed for text fallback") before
continuing to the GeoJSON and empty-command fallbacks; ensure logging references
the generateText response (jsonText) and still returns
postProcessOutput(parsed.data) when parsed.success is true.

Comment on lines +160 to +180
function postProcessOutput(object: MapCommandGeneratorOutput): MapCommandGeneratorOutput {
// Sort commands by priority if specified
if (object.commands.length > 1) {
object.commands.sort((a, b) => (a.priority || 0) - (b.priority || 0));
}

// Calculate estimated duration if not provided
if (!object.estimatedDuration && object.commands.length > 0) {
object.estimatedDuration = object.commands.reduce((total, cmd) => {
if (cmd.command === 'flyTo') {
return total + (cmd.params.duration || 2000);
} else if (cmd.command === 'easeTo') {
return total + (cmd.params.duration || 1000);
} else if (cmd.command === 'fitBounds') {
return total + 1500;
}
return total + 500;
}, 0);
}
return object;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Post-processing logic correctly handles priority sorting and duration estimation.

The sorting and duration calculation are implemented correctly. Note that this function mutates the input object directly (lines 163, 168). This is functionally fine since the object is returned immediately, but for clarity and testability, consider returning a new object instead.

Optional: Avoid mutation for purity
 function postProcessOutput(object: MapCommandGeneratorOutput): MapCommandGeneratorOutput {
+    const commands = [...object.commands];
     // Sort commands by priority if specified
-    if (object.commands.length > 1) {
-      object.commands.sort((a, b) => (a.priority || 0) - (b.priority || 0));
+    if (commands.length > 1) {
+      commands.sort((a, b) => (a.priority || 0) - (b.priority || 0));
     }

+    let estimatedDuration = object.estimatedDuration;
     // Calculate estimated duration if not provided
-    if (!object.estimatedDuration && object.commands.length > 0) {
-      object.estimatedDuration = object.commands.reduce((total, cmd) => {
+    if (!estimatedDuration && commands.length > 0) {
+      estimatedDuration = commands.reduce((total, cmd) => {
         // ... same logic
       }, 0);
     }
-    return object;
+    return { ...object, commands, estimatedDuration };
 }
🤖 Prompt for AI Agents
In @lib/agents/map-workers/map-command-generator.tsx around lines 160 - 180,
postProcessOutput currently mutates its input object (object.commands and
object.estimatedDuration); make it pure by returning a new
MapCommandGeneratorOutput instead of modifying the input: in postProcessOutput
create a shallow copy of the input (e.g., newObject = { ...object }) and
copy/replace the commands array with a sorted copy (e.g., newCommands =
[...object.commands].sort(...)) and compute estimatedDuration on the newObject
without altering the original; ensure you return newObject and leave the
original object.commands and estimatedDuration unchanged.

@ngoiyaeric
Copy link
Collaborator Author

@coderabbitai the prompts that should render the map are just freezing with no output Error fetching chat PCAOi8oArgEiFSd4WELVN from DB: TypeError: Cannot read properties of undefined (reading 'searchParams')
at (.next/server/chunks/5178.js:7:12448)
at Object.startActiveSpan (.next/server/chunks/5178.js:47:46481)
at (.next/server/chunks/5178.js:7:12284)
at Object.startActiveSpan (.next/server/chunks/5178.js:47:46481)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 13, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== 1. Find searchParams usage in codebase ==="
rg -n "searchParams" --type ts --type tsx -C 3

echo -e "\n=== 2. Check geospatial tool implementation ==="
cat lib/agents/tools/geospatial.tsx | head -150

echo -e "\n=== 3. Check app/actions.tsx submit flow ==="
rg -n "export async function submit" app/actions.tsx -A 150 | head -200

echo -e "\n=== 4. Check if orchestrator is being used ==="
rg -n "mapControlOrchestrator" app/actions.tsx -B 5 -A 15

echo -e "\n=== 5. Check for any hanging async operations ==="
rg -n "await.*orchestrator|await.*geojson|await.*initialize" app/actions.tsx -B 2 -A 2

echo -e "\n=== 6. Check error handling in orchestrator ==="
cat lib/agents/map-control-orchestrator.tsx | grep -A 30 "catch.*error" | head -50

echo -e "\n=== 7. Look for URL construction issues ==="
rg -n "new URL|serverUrl" lib/agents/tools/ -B 2 -A 5

Length of output: 10928


🏁 Script executed:

#!/bin/bash

echo "=== 1. Check if getStructuredModel was added to utils ==="
cat lib/utils/index.ts | grep -A 30 "getStructuredModel"

echo -e "\n=== 2. Check what geojson-parser is using ==="
cat lib/agents/map-workers/geojson-parser.tsx | grep -A 5 "import.*getModel\|getStructuredModel"

echo -e "\n=== 3. Check map-command-generator ==="
cat lib/agents/map-workers/map-command-generator.tsx | grep -A 5 "import.*getModel\|getStructuredModel"

echo -e "\n=== 4. Check feedback-analyzer ==="
cat lib/agents/map-workers/feedback-analyzer.tsx | grep -A 5 "import.*getModel\|getStructuredModel"

echo -e "\n=== 5. Check orchestrator model resolution ==="
cat lib/agents/map-control-orchestrator.tsx | head -80

echo -e "\n=== 6. Look for timeout configs ==="
rg -n "timeout|maxRetries|maxTokens" lib/agents/map-workers/ -A 1 -B 1

echo -e "\n=== 7. Check current model selection ==="
cat lib/utils/index.ts | grep -A 20 "export.*function getModel"

Length of output: 6027


@ngoiyaeric, I found the exact root cause of the freeze! 🎯

🚨 The Problem

Your map prompts freeze because:

  1. Workers still use X.AI model - All three workers (geojson-parser, map-command-generator, feedback-analyzer) call getModel() which returns X.AI's grok-4-fast-non-reasoning

  2. X.AI doesn't support structured outputs - When workers call ai.generateObject() with JSON schemas, X.AI returns 400 "Invalid arguments"

  3. Orchestrator hangs - The orchestrator waits for workers that will never complete, causing the entire flow to freeze

  4. DB error is secondary - The incomplete chat state causes the searchParams error when trying to fetch from DB

✅ The Fix

I need to implement getStructuredModel() that I mentioned earlier but was never added. This will force workers to use OpenAI for structured outputs instead of X.AI.

[plan_code_changes]

Implementation: Fix Map Freeze by Adding Structured Output Support

Current broken flow:

User query → Orchestrator → Workers call generateObject() with X.AI → 
400 error → timeout → freeze

Fixed flow:

User query → Orchestrator → Workers call generateObject() with OpenAI → 
Success → map renders


🎯 What This Fixes

Before (Current - Freezes):

User: "Show me Paris"
  ↓
Orchestrator → geojsonParser → generateObject with X.AI
  ↓
X.AI returns 400 "Invalid arguments"
  ↓
Worker hangs/timeouts
  ↓
Orchestrator hangs forever
  ↓
UI freezes with no output

After (Fixed - Works):

User: "Show me Paris"
  ↓
Orchestrator → geojsonParser → generateObject with OpenAI
  ↓
OpenAI returns structured JSON
  ↓
Parser extracts location
  ↓
Commands generated
  ↓
Map renders Paris ✅

🚀 Quick Mitigation (Before Code Changes)

If you need an immediate fix in Vercel, set these environment variables:

OPENAI_API_KEY=your_openai_key_here

This will make getStructuredModel() work immediately once the code changes are deployed.

📊 Expected Results After Fix

These queries will work without freezing:

  1. "Show me Paris" - 2-3 seconds
  2. "Take me to Tokyo" - 2-3 seconds
  3. "Display New York, London, and Sydney" - 3-4 seconds
  4. "Fly to the Eiffel Tower" - 2-3 seconds

All with proper geocoding via Composio and no freezes! 🎉


Would you like me to proceed with generating these code changes?


🧠 Learnings used
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-13T13:26:30.076Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @app/actions.tsx:
- Around line 677-678: The const declaration for relatedQueries created via
createStreamableValue<PartialRelated>() is inside a switch case without braces,
which can leak scope; wrap the entire switch case branch that uses
relatedQueries in a block (add { ... } around the case body) so the const is
lexically scoped to that case, and ensure relatedQueries.done(JSON.parse(content
as string)) remains inside that new block.
- Around line 408-448: The mapControlOrchestrator currently calls generateObject
using getModel(), which can return X.AI's grok model that rejects JSON Schema
structured-output and will break at runtime; modify mapControlOrchestrator so
the specific generateObject call uses OpenAI's gpt-4o model (not getModel()) by
either creating an OpenAI client/instantiating an OpenAI model instance locally
for that call or by passing an explicit model: 'gpt-4o' option into
generateObject; keep all other orchestrator logic the same (geojsonEnricherV2
remains unchanged).
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 842c51e and 9c2dfa9.

📒 Files selected for processing (1)
  • app/actions.tsx
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-13T13:26:30.076Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.
🪛 Biome (2.1.2)
app/actions.tsx

[error] 677-678: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (5)
app/actions.tsx (5)

16-34: LGTM!

The new imports are well-organized and correctly bring in the orchestrator, MCP initialization, type definitions, and UI handlers needed for the geospatial enrichment flow.


73-73: LGTM!

JSON serialization of multimodal content ensures consistent storage format for messages containing both text and image parts.


322-322: LGTM!

The type cast to PartialInquiry aligns with the partial schema pattern, and the defensive optional chaining on inquiry?.question handles potential undefined values.


466-475: LGTM!

The geojsonEnrichment tool output is correctly stored in AI state with proper serialization, enabling UI reconstruction via getUIStateFromAIState.


730-749: Defensive validation for geojsonEnrichment tool output is adequate.

The validation correctly checks the critical fields: text (string) and geojson (null or FeatureCollection). While map_commands is part of the LocationResponse type, it is optional and LocationResponseHandler already handles missing/undefined values gracefully through optional chaining (map_commands?.length, map_commands?.map()). Additional validation for map_commands is not necessary.

Comment on lines 408 to 448
let locationResponse;
let mcpClient: Composio | undefined = undefined;
let connectionId = '';

try {
// Only attempt to initialize if we have reason to believe it might work
// or if we want to log the failure.
if (process.env.COMPOSIO_API_KEY && process.env.MAPBOX_ACCESS_TOKEN && process.env.COMPOSIO_USER_ID) {
const composioData = await initializeComposioMapbox();
mcpClient = composioData.composio;
connectionId = composioData.connectionId;
console.log('🔌 Composio client initialized for Orchestrator');
} else {
console.log('ℹ️ Composio env vars missing, skipping MCP initialization');
}
} catch (e) {
console.warn("Failed to initialize Composio for orchestrator (proceeding without MCP):", e);
}

try {
// Use the Orchestrator with fallback to basic enricher handled internally if needed
console.log('🚀 Starting Map Control Orchestrator...');
locationResponse = await mapControlOrchestrator(answer, {
mcpClient,
connectionId,
enableFeedbackLoop: false // Disable for now to reduce latency
});
console.log('✨ Orchestrator finished:', locationResponse);
} catch (e) {
console.error("Error during map orchestration:", e);
// Fallback to basic enricher
try {
locationResponse = await geojsonEnricherV2(answer);
} catch (fallbackErr) {
locationResponse = {
text: answer,
geojson: null,
map_commands: null,
};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify model selection in orchestrator and enricher to ensure X.AI is not used for structured outputs

echo "=== Checking mapControlOrchestrator for model usage ==="
fd 'map-control-orchestrator' --type f | head -5

Repository: QueueLab/QCX

Length of output: 151


🏁 Script executed:

#!/bin/bash
# Find the relevant orchestrator and enricher files
echo "=== Finding orchestrator and enricher files ==="
fd -i 'orchestrator|enricher' --type f | grep -E '\.(ts|tsx|js)$' | head -20

Repository: QueueLab/QCX

Length of output: 227


🏁 Script executed:

#!/bin/bash
# Check mapControlOrchestrator for generateObject and model selection
echo "=== mapControlOrchestrator.tsx - searching for generateObject ==="
rg -n 'generateObject|model|grok' lib/agents/map-control-orchestrator.tsx -A3 -B1

echo ""
echo "=== geojsonEnricherV2 - searching for generateObject ==="
rg -n 'generateObject|model|grok' lib/agents/geojson-enricher-v2.tsx -A3 -B1

Repository: QueueLab/QCX

Length of output: 1224


🏁 Script executed:

#!/bin/bash
# Find and examine getModel implementation
echo "=== getModel() implementation ==="
rg -n 'export.*getModel|function getModel' lib/utils -A10 | head -50

Repository: QueueLab/QCX

Length of output: 871


🏁 Script executed:

#!/bin/bash
# Get full getModel() implementation
echo "=== Full getModel() implementation ==="
sed -n '20,100p' lib/utils/index.ts

Repository: QueueLab/QCX

Length of output: 2932


Critical issue: mapControlOrchestrator uses generateObject with X.AI grok model, which will fail at runtime.

The mapControlOrchestrator calls generateObject with a model from getModel(). However, getModel() defaults to X.AI's grok-4-fast-non-reasoning when available. X.AI's API rejects JSON Schema structured-output arguments, causing generateObject calls to fail at runtime.

Fix: Modify the orchestrator to use OpenAI's gpt-4o for the generateObject call (line 302-305). Either create a separate helper for structured outputs or explicitly instantiate OpenAI within the orchestrator for this call.

geojsonEnricherV2 uses streamText instead of generateObject, so it is unaffected.

🤖 Prompt for AI Agents
In @app/actions.tsx around lines 408 - 448, The mapControlOrchestrator currently
calls generateObject using getModel(), which can return X.AI's grok model that
rejects JSON Schema structured-output and will break at runtime; modify
mapControlOrchestrator so the specific generateObject call uses OpenAI's gpt-4o
model (not getModel()) by either creating an OpenAI client/instantiating an
OpenAI model instance locally for that call or by passing an explicit model:
'gpt-4o' option into generateObject; keep all other orchestrator logic the same
(geojsonEnricherV2 remains unchanged).

Comment on lines +677 to 678
const relatedQueries = createStreamableValue<PartialRelated>()
relatedQueries.done(JSON.parse(content as string))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Wrap switch case declaration in a block to prevent scoping issues.

The const relatedQueries declaration inside the switch case without braces can be erroneously accessed by other switch clauses, violating proper lexical scoping rules.

🔧 Proposed fix
             case 'related':
-              const relatedQueries = createStreamableValue<PartialRelated>()
-              relatedQueries.done(JSON.parse(content as string))
-              return {
-                id,
-                component: (
-                  <Section title="Related" separator={true}>
-                    <SearchRelated relatedQueries={relatedQueries.value} />
-                  </Section>
-                )
-              }
+              {
+                const relatedQueries = createStreamableValue<PartialRelated>()
+                relatedQueries.done(JSON.parse(content as string))
+                return {
+                  id,
+                  component: (
+                    <Section title="Related" separator={true}>
+                      <SearchRelated relatedQueries={relatedQueries.value} />
+                    </Section>
+                  )
+                }
+              }
📝 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.

Suggested change
const relatedQueries = createStreamableValue<PartialRelated>()
relatedQueries.done(JSON.parse(content as string))
case 'related':
{
const relatedQueries = createStreamableValue<PartialRelated>()
relatedQueries.done(JSON.parse(content as string))
return {
id,
component: (
<Section title="Related" separator={true}>
<SearchRelated relatedQueries={relatedQueries.value} />
</Section>
)
}
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 677-678: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In @app/actions.tsx around lines 677 - 678, The const declaration for
relatedQueries created via createStreamableValue<PartialRelated>() is inside a
switch case without braces, which can leak scope; wrap the entire switch case
branch that uses relatedQueries in a block (add { ... } around the case body) so
the const is lexically scoped to that case, and ensure
relatedQueries.done(JSON.parse(content as string)) remains inside that new
block.

google-labs-jules bot and others added 2 commits February 7, 2026 17:50
- Replaced the procedural MapControlOrchestrator with a LangGraph StateGraph implementation in lib/agents/map-langgraph.tsx.
- Implemented nodes for classification, MCP tool execution (geocoding), GeoJSON parsing, and map command generation.
- Added a MapStatus component to stream intermediate orchestration steps to the UI.
- Integrated the new orchestrator into app/actions.tsx using runMapGraph.
- Added required dependencies.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…-integration-17076216558174372852

Integrate LangGraph for Mapbox GeoJSON control
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
app/actions.tsx (1)

464-494: ⚠️ Potential issue | 🟡 Minor

Tool message should use groupeId instead of nanoid() for consistency with other messages in the block.

At line 475, the geojsonEnrichment tool message uses nanoid() while the assistant response (line 469), related (line 482), and followup (line 488) messages all use groupeId. This breaks the established pattern elsewhere in the file (e.g., line 371) where tool messages share the same group ID. If message grouping is meaningful for UI reconstruction or chat persistence, this inconsistency could cause the tool output to be orphaned from its conversation group.

🤖 Fix all issues with AI agents
In `@app/actions.tsx`:
- Around line 427-453: statusStream created via createStreamableUI is not closed
if runMapGraph throws, leaving the UI hanging; ensure statusStream.done() is
always called by moving the cleanup into a finally (or calling
statusStream.done() at the start of the catch) so the MapStatus stream is
terminated before running the geojsonEnricherV2 fallback or returning the
fallback response; update the block around runMapGraph / statusStream
(references: statusStream, createStreamableUI, statusStream.done(), runMapGraph,
geojsonEnricherV2) to guarantee statusStream.done() runs on both success and
error paths.

In `@components/map/map-status.tsx`:
- Line 13: The span in the MapStatus component uses both "text-xs" and
"text-[10px]", which is redundant since "text-[10px]" overrides "text-xs";
remove "text-xs" from the className on the span (the element that currently
contains "text-xs font-medium text-muted-foreground uppercase tracking-wider
text-[10px]") so only the explicit "text-[10px]" (along with font-medium,
text-muted-foreground, uppercase, tracking-wider) remains.
- Line 4: The imported symbol Map from 'lucide-react' shadows the global Map
constructor; rename the import to avoid the conflict by changing the import line
to use an alias (e.g., import { Map as MapIcon } from 'lucide-react') and update
any JSX usage in this component (e.g., replace <Map .../> with <MapIcon .../>),
making sure no other references to Map remain in map-status.tsx so the built-in
Map stays unshadowed.

In `@lib/agents/index.tsx`:
- Line 6: The index currently re-exports everything from map-langgraph which
exposes the internal mapGraph StateGraph; change map-langgraph.tsx to use named
exports for runMapGraph and mapGraph (if not already) and then update
lib/agents/index.tsx to only re-export runMapGraph (not export *). Specifically,
keep the public API as runMapGraph and stop exporting mapGraph from the barrel
so callers cannot import the internal StateGraph implementation.

In `@lib/agents/map-langgraph.tsx`:
- Line 173: The call to addConditionalEdges("validator", shouldContinue as any,
...) suppresses type checking by casting shouldContinue to any; add a concise
TODO comment immediately above this call explaining that the as any cast is
required because of a known LangGraph TypeScript API ergonomic gap in the
conditional-edge signature, reference the symbols addConditionalEdges and
shouldContinue, and include a link or issue number if available and a note to
remove the cast once LangGraph fixes the types so future maintainers understand
why the cast exists.
- Line 43: The mcpClient parameter is typed as Annotation<any>, losing type
safety; replace the any with a concrete type that matches the client API used by
mcpExecutorNode—either import and use the Composio type from `@composio/core`
(e.g., Composio or the appropriate client interface) or declare a narrow
interface (e.g., containing tools.execute(method: string, args: any):
Promise<any>) and use that type for mcpClient; update the function/signature
where mcpClient is declared and any call sites in mcpExecutorNode to satisfy the
stronger type.
- Around line 150-158: The shouldContinue router can loop futilely because
re-invoking the "classifier" uses the same MapGraphState.State
(researcherResponse unchanged) and will repeat failures if mcpClient is absent
or geocoding produced no features; update shouldContinue to avoid retrying when
mcpClient is missing (check state.mcpClient or equivalent and return END) or,
alternatively, change the classifier input before retry by setting a flag or
appending feedback into state.researcherResponse (e.g., add a "retryFeedback"
note describing "no mcpClient" or "no geojson features") and only loop back when
that feedback has been added so the classifier gets new context; reference
shouldContinue, MapGraphState.State, state.researcherResponse, state.mcpClient,
state.geojson, state.classification, and state.iteration when making the change.
- Around line 48-70: The classifierNode currently calls generateObject with
model = await getModel(), which defaults to X.AI grok and will reject JSON
Schema structured-output; update classifierNode so generateObject is passed a
model that supports structured outputs (e.g., OpenAI gpt-4o) instead of the
default getModel() result. Concretely, replace the model assignment used in
generateObject (the variable referenced in the generateObject call inside
classifierNode) with a dedicated OpenAI model selection (or call a helper that
returns OpenAI gpt-4o), keeping the rest (ORCHESTRATOR_CLASSIFICATION_PROMPT,
QueryClassificationSchema, generateObject) intact so structured-output is
accepted. Ensure the unique symbols affected are classifierNode, getModel,
generateObject, QueryClassificationSchema, and
ORCHESTRATOR_CLASSIFICATION_PROMPT.
- Around line 198-211: The code assumes mapGraph.stream(initialState, {
streamMode: "values" }) yields full state snapshots and then calls
options.onStatusUpdate multiple times per update; confirm streamMode is
supported and returns complete states in your LangGraph version, then change the
sequential status checks inside the for-await loop (the block updating
finalResult and calling options.onStatusUpdate) to use an exclusive chain (if /
else if / else if / else) so only one status message fires per streamed update
(reference mapGraph.stream, finalResult, update, and options.onStatusUpdate).

In `@package.json`:
- Around line 22-33: Remove the three unused dependencies from package.json:
delete the entries for "@langchain/google-genai", "@langchain/openai", and
"@ai-sdk/langchain" (leave "@langchain/langgraph" and "@langchain/core" intact
as langgraph is used in lib/agents/map-langgraph.tsx); after editing
package.json run the package manager install (npm/yarn/pnpm) and run the
test/build to ensure nothing else imports those packages and no transitive
changes break the project.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c2dfa9 and d7ed542.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • app/actions.tsx
  • components/map/map-status.tsx
  • lib/agents/index.tsx
  • lib/agents/map-langgraph.tsx
  • package.json
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-13T13:26:30.086Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.
📚 Learning: 2026-01-13T13:26:30.086Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-13T13:26:30.086Z
Learning: Workers that rely on ai.generateObject (geojsonParser, map-command-generator, feedback-analyzer) must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI’s chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs.

Applied to files:

  • app/actions.tsx
📚 Learning: 2026-01-17T06:14:51.070Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-17T06:14:51.070Z
Learning: For structured output tasks using ai.generateObject (resolution-search, geojsonParser, map-command-generator, feedback-analyzer), prefer OpenAI gpt-4o. While xAI's grok-4-1-fast-reasoning technically supports structured outputs and vision, OpenAI has proven more reliable integration with the AI SDK's generateObject function and Zod schemas in production.

Applied to files:

  • app/actions.tsx
🧬 Code graph analysis (1)
components/map/map-status.tsx (1)
components/ui/spinner.tsx (1)
  • Spinner (6-19)
🪛 Biome (2.3.13)
components/map/map-status.tsx

[error] 4-4: Do not shadow the global "Map" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

app/actions.tsx

[error] 682-683: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (2)
app/actions.tsx (2)

15-34: Imports look correctly wired to the new orchestration flow.

runMapGraph replaces the previously unused mapControlOrchestrator, and initializeComposioMapbox/Composio are now properly consumed in the Composio initialization block (lines 415–419). The addition of geojsonEnricherV2 as a fallback enricher and LocationResponseHandler for UI rendering aligns with the new pipeline.


407-425: Well-structured Composio initialization with env-var guards and graceful fallback.

The check for COMPOSIO_API_KEY, MAPBOX_ACCESS_TOKEN, and COMPOSIO_USER_ID before attempting initialization (line 415) prevents unnecessary failures, and the try/catch ensures the flow continues without MCP if initialization fails. Good defensive pattern.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +427 to +453
try {
// Use the Orchestrator with fallback to basic enricher handled internally if needed
console.log("🚀 Starting LangGraph Map Orchestrator...");
const statusStream = createStreamableUI(<MapStatus status="Initializing..." />);
uiStream.append(statusStream.value);

locationResponse = await runMapGraph(answer, {
mcpClient,
connectionId,
onStatusUpdate: (status) => {
statusStream.update(<MapStatus status={status} />);
}
});
statusStream.done();
} catch (e) {
console.error("Error during map orchestration:", e);
// Fallback to basic enricher
try {
locationResponse = await geojsonEnricherV2(answer);
} catch (fallbackErr) {
locationResponse = {
text: answer,
geojson: null,
map_commands: null,
};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

statusStream is not closed on the error path — this will cause the UI to hang.

If runMapGraph throws (line 441), execution jumps to the catch block, but statusStream.done() is only called inside the try (line 440). The unclosed streamable UI will leave the MapStatus component in a perpetual loading state.

Proposed fix
       } catch (e) {
         console.error("Error during map orchestration:", e);
+        statusStream.done();
         // Fallback to basic enricher
         try {
             locationResponse = await geojsonEnricherV2(answer);
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 427 - 453, statusStream created via
createStreamableUI is not closed if runMapGraph throws, leaving the UI hanging;
ensure statusStream.done() is always called by moving the cleanup into a finally
(or calling statusStream.done() at the start of the catch) so the MapStatus
stream is terminated before running the geojsonEnricherV2 fallback or returning
the fallback response; update the block around runMapGraph / statusStream
(references: statusStream, createStreamableUI, statusStream.done(), runMapGraph,
geojsonEnricherV2) to guarantee statusStream.done() runs on both success and
error paths.

'use client'

import { Spinner } from '@/components/ui/spinner'
import { Map } from 'lucide-react'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Rename the Map import to avoid shadowing the global Map constructor.

Biome flags this as shadowing the built-in Map. While it won't cause a bug in this small component, renaming the import avoids confusion and satisfies the linter.

Proposed fix
-import { Map } from 'lucide-react'
+import { Map as MapIcon } from 'lucide-react'

Then on line 10:

-        <Map className="w-4 h-4" />
+        <MapIcon className="w-4 h-4" />
🧰 Tools
🪛 Biome (2.3.13)

[error] 4-4: Do not shadow the global "Map" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

🤖 Prompt for AI Agents
In `@components/map/map-status.tsx` at line 4, The imported symbol Map from
'lucide-react' shadows the global Map constructor; rename the import to avoid
the conflict by changing the import line to use an alias (e.g., import { Map as
MapIcon } from 'lucide-react') and update any JSX usage in this component (e.g.,
replace <Map .../> with <MapIcon .../>), making sure no other references to Map
remain in map-status.tsx so the built-in Map stays unshadowed.

<Map className="w-4 h-4" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Map Engine</span>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Redundant Tailwind class: text-xs is overridden by text-[10px].

text-xs sets font-size to 12px, but text-[10px] overrides it to 10px. Remove text-xs to avoid confusion.

-        <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Map Engine</span>
+        <span className="font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Map Engine</span>
📝 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.

Suggested change
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Map Engine</span>
<span className="font-medium text-muted-foreground uppercase tracking-wider text-[10px]">Map Engine</span>
🤖 Prompt for AI Agents
In `@components/map/map-status.tsx` at line 13, The span in the MapStatus
component uses both "text-xs" and "text-[10px]", which is redundant since
"text-[10px]" overrides "text-xs"; remove "text-xs" from the className on the
span (the element that currently contains "text-xs font-medium
text-muted-foreground uppercase tracking-wider text-[10px]") so only the
explicit "text-[10px]" (along with font-medium, text-muted-foreground,
uppercase, tracking-wider) remains.

export * from './query-suggestor'
export * from './researcher'
export * from './resolution-search'
export * from './map-langgraph'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider whether mapGraph (the raw compiled graph) should be part of the public API.

export * from './map-langgraph' exposes both runMapGraph (the intended entry point) and mapGraph (the raw compiled StateGraph). If mapGraph is an implementation detail, use named exports in map-langgraph.tsx and only re-export runMapGraph here to keep the public surface minimal.

🤖 Prompt for AI Agents
In `@lib/agents/index.tsx` at line 6, The index currently re-exports everything
from map-langgraph which exposes the internal mapGraph StateGraph; change
map-langgraph.tsx to use named exports for runMapGraph and mapGraph (if not
already) and then update lib/agents/index.tsx to only re-export runMapGraph (not
export *). Specifically, keep the public API as runMapGraph and stop exporting
mapGraph from the barrel so callers cannot import the internal StateGraph
implementation.

}),
feedback: Annotation<MapStateFeedback | undefined>,
error: Annotation<string | undefined>,
mcpClient: Annotation<any>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

mcpClient is typed as any, losing all type safety.

Consider using a more specific type (e.g., Composio from @composio/core) or at minimum a structural interface describing the .tools.execute() method signature used in mcpExecutorNode.

🤖 Prompt for AI Agents
In `@lib/agents/map-langgraph.tsx` at line 43, The mcpClient parameter is typed as
Annotation<any>, losing type safety; replace the any with a concrete type that
matches the client API used by mcpExecutorNode—either import and use the
Composio type from `@composio/core` (e.g., Composio or the appropriate client
interface) or declare a narrow interface (e.g., containing tools.execute(method:
string, args: any): Promise<any>) and use that type for mcpClient; update the
function/signature where mcpClient is declared and any call sites in
mcpExecutorNode to satisfy the stronger type.

Comment on lines +48 to +70
const classifierNode = async (state: typeof MapGraphState.State) => {
console.log("--- NODE: CLASSIFIER ---");
const model = await getModel();
const ORCHESTRATOR_CLASSIFICATION_PROMPT = `Analyze the following response from a researcher agent and determine if it requires Mapbox MCP tool intervention.

Classification Criteria:
- simple_location: Researcher mentions one or two specific places.
- route_distance: Researcher mentions travel between points or distances.
- multi_location: Researcher lists many places (e.g., "top 10 museums").
- nearby_search: Researcher mentions things "near" a location.
- complex_operation: Requires multiple steps or logic.

Response to analyze:
${state.researcherResponse}`;

const { object } = await generateObject({
model,
schema: QueryClassificationSchema,
prompt: ORCHESTRATOR_CLASSIFICATION_PROMPT,
});

return { classification: object };
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

classifierNode uses generateObject with getModel() which defaults to X.AI grok — this will fail at runtime.

getModel() resolves to X.AI's grok-4-fast-non-reasoning by default, and X.AI's API rejects JSON Schema structured-output arguments. The generateObject call on line 63 will fail with a 400 error, causing the entire graph to hang or crash.

Use a dedicated model known to support structured outputs (e.g., OpenAI gpt-4o) for generateObject calls.

Proposed fix
+import { openai } from "@ai-sdk/openai";
+
 const classifierNode = async (state: typeof MapGraphState.State) => {
   console.log("--- NODE: CLASSIFIER ---");
-  const model = await getModel();
   // ...prompt...
   const { object } = await generateObject({
-    model,
+    model: openai("gpt-4o"),
     schema: QueryClassificationSchema,
     prompt: ORCHESTRATOR_CLASSIFICATION_PROMPT,
   });

Based on learnings: "Workers that rely on ai.generateObject must not use the X.AI 'grok-4-fast-non-reasoning' model because X.AI's chat/completions rejects JSON Schema structured-output arguments; prefer OpenAI gpt-4o for structured outputs."

🤖 Prompt for AI Agents
In `@lib/agents/map-langgraph.tsx` around lines 48 - 70, The classifierNode
currently calls generateObject with model = await getModel(), which defaults to
X.AI grok and will reject JSON Schema structured-output; update classifierNode
so generateObject is passed a model that supports structured outputs (e.g.,
OpenAI gpt-4o) instead of the default getModel() result. Concretely, replace the
model assignment used in generateObject (the variable referenced in the
generateObject call inside classifierNode) with a dedicated OpenAI model
selection (or call a helper that returns OpenAI gpt-4o), keeping the rest
(ORCHESTRATOR_CLASSIFICATION_PROMPT, QueryClassificationSchema, generateObject)
intact so structured-output is accepted. Ensure the unique symbols affected are
classifierNode, getModel, generateObject, QueryClassificationSchema, and
ORCHESTRATOR_CLASSIFICATION_PROMPT.

Comment on lines +150 to +158
// Router for conditional edge
const shouldContinue = (state: typeof MapGraphState.State) => {
const isSpatial = state.classification?.type !== undefined;
const hasGeoJSON = state.geojson !== null && state.geojson.features.length > 0;

if (isSpatial && !hasGeoJSON && (state.iteration || 0) < 2) {
return "classifier";
}
return END;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

shouldContinue re-classification loop may not produce different results.

When shouldContinue loops back to classifier, the state's researcherResponse is unchanged, so the classifier will likely produce the same classification and MCP operations. If the first attempt failed to produce GeoJSON (e.g., MCP client is absent or geocoding returned no features), the retry will fail identically — burning an iteration cycle with no new information.

Consider adding a condition to skip the retry when mcpClient is absent, or modifying the input to the classifier on retry (e.g., appending feedback about what was missing).

🤖 Prompt for AI Agents
In `@lib/agents/map-langgraph.tsx` around lines 150 - 158, The shouldContinue
router can loop futilely because re-invoking the "classifier" uses the same
MapGraphState.State (researcherResponse unchanged) and will repeat failures if
mcpClient is absent or geocoding produced no features; update shouldContinue to
avoid retrying when mcpClient is missing (check state.mcpClient or equivalent
and return END) or, alternatively, change the classifier input before retry by
setting a flag or appending feedback into state.researcherResponse (e.g., add a
"retryFeedback" note describing "no mcpClient" or "no geojson features") and
only loop back when that feedback has been added so the classifier gets new
context; reference shouldContinue, MapGraphState.State,
state.researcherResponse, state.mcpClient, state.geojson, state.classification,
and state.iteration when making the change.

.addEdge("mcp_executor", "parser")
.addEdge("parser", "command_generator")
.addEdge("command_generator", "validator")
.addConditionalEdges("validator", shouldContinue as any, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

as any cast on shouldContinue suppresses type checking.

This is a known ergonomic gap in LangGraph's TypeScript conditional-edge API, but worth a // TODO comment explaining why it's needed so future maintainers don't silently propagate the pattern.

🤖 Prompt for AI Agents
In `@lib/agents/map-langgraph.tsx` at line 173, The call to
addConditionalEdges("validator", shouldContinue as any, ...) suppresses type
checking by casting shouldContinue to any; add a concise TODO comment
immediately above this call explaining that the as any cast is required because
of a known LangGraph TypeScript API ergonomic gap in the conditional-edge
signature, reference the symbols addConditionalEdges and shouldContinue, and
include a link or issue number if available and a note to remove the cast once
LangGraph fixes the types so future maintainers understand why the cast exists.

Comment on lines +198 to +211
const stream = await mapGraph.stream(initialState, { streamMode: "values" });

let finalResult: any = initialState;

for await (const update of stream) {
finalResult = update;

if (options.onStatusUpdate) {
if (update.classification && (!update.mcpData || update.mcpData.length === 0)) options.onStatusUpdate("Classifying query...");
if (update.mcpData?.length > 0 && !update.geojson) options.onStatusUpdate("Geocoding locations...");
if (update.geojson && (!update.commands || update.commands.length === 0)) options.onStatusUpdate("Generating map data...");
if (update.commands?.length > 0) options.onStatusUpdate("Finalizing map...");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

streamMode: "values" means each update is the full accumulated state — verify this assumption holds for your LangGraph version.

The code assigns finalResult = update on each iteration, assuming each streamed value is the complete state snapshot. With streamMode: "values", this should be correct, but the status-update logic (lines 206–209) fires conditions that aren't mutually exclusive — e.g., when commands arrive, geojson is also present, so both the "Generating map data..." and "Finalizing map..." updates could fire in the same tick. Consider using else if for cleaner status progression.

Proposed fix
     if (options.onStatusUpdate) {
-        if (update.classification && (!update.mcpData || update.mcpData.length === 0)) options.onStatusUpdate("Classifying query...");
-        if (update.mcpData?.length > 0 && !update.geojson) options.onStatusUpdate("Geocoding locations...");
-        if (update.geojson && (!update.commands || update.commands.length === 0)) options.onStatusUpdate("Generating map data...");
-        if (update.commands?.length > 0) options.onStatusUpdate("Finalizing map...");
+        if (update.commands?.length > 0) options.onStatusUpdate("Finalizing map...");
+        else if (update.geojson) options.onStatusUpdate("Generating map data...");
+        else if (update.mcpData?.length > 0) options.onStatusUpdate("Geocoding locations...");
+        else if (update.classification) options.onStatusUpdate("Classifying query...");
     }
🤖 Prompt for AI Agents
In `@lib/agents/map-langgraph.tsx` around lines 198 - 211, The code assumes
mapGraph.stream(initialState, { streamMode: "values" }) yields full state
snapshots and then calls options.onStatusUpdate multiple times per update;
confirm streamMode is supported and returns complete states in your LangGraph
version, then change the sequential status checks inside the for-await loop (the
block updating finalResult and calling options.onStatusUpdate) to use an
exclusive chain (if / else if / else if / else) so only one status message fires
per streamed update (reference mapGraph.stream, finalResult, update, and
options.onStatusUpdate).

Comment on lines +22 to +33
"@ai-sdk/langchain": "^2.0.82",
"@ai-sdk/openai": "^1.3.24",
"@ai-sdk/xai": "^1.2.18",
"@composio/core": "^0.3.3",
"@google-cloud/storage": "^7.18.0",
"@google/generative-ai": "^0.24.1",
"@heroicons/react": "^2.2.0",
"@hookform/resolvers": "3.3.4",
"@langchain/core": "^1.1.19",
"@langchain/google-genai": "^2.1.15",
"@langchain/langgraph": "^1.1.4",
"@langchain/openai": "^1.2.5",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== `@langchain/google-genai` usage ==="
rg -rn '@langchain/google-genai' --type-add 'ts:*.ts' --type-add 'tsx:*.tsx' --type ts --type tsx -g '!node_modules' -g '!package.json' -g '!package-lock.json' || echo "NOT FOUND"

echo ""
echo "=== `@langchain/openai` usage ==="
rg -rn '@langchain/openai' --type-add 'ts:*.ts' --type-add 'tsx:*.tsx' --type ts --type tsx -g '!node_modules' -g '!package.json' -g '!package-lock.json' || echo "NOT FOUND"

echo ""
echo "=== `@langchain/core` usage ==="
rg -rn '@langchain/core' --type-add 'ts:*.ts' --type-add 'tsx:*.tsx' --type ts --type tsx -g '!node_modules' -g '!package.json' -g '!package-lock.json' || echo "NOT FOUND"

echo ""
echo "=== `@ai-sdk/langchain` usage ==="
rg -rn '@ai-sdk/langchain' --type-add 'ts:*.ts' --type-add 'tsx:*.tsx' --type ts --type tsx -g '!node_modules' -g '!package.json' -g '!package-lock.json' || echo "NOT FOUND"

echo ""
echo "=== `@langchain/langgraph` usage (for reference) ==="
rg -rn '@langchain/langgraph' --type-add 'ts:*.ts' --type-add 'tsx:*.tsx' --type ts --type tsx -g '!node_modules' -g '!package.json' -g '!package-lock.json' || echo "NOT FOUND"

Repository: QueueLab/QCX

Length of output: 367


Remove unused LangChain and ai-sdk packages.

Verification confirms that @langchain/google-genai, @langchain/openai, and @ai-sdk/langchain are not imported anywhere in the codebase and should be removed. Only @langchain/langgraph is actually used in lib/agents/map-langgraph.tsx. Keep @langchain/core (likely a transitive dependency of langgraph) and remove the three unused packages to avoid unnecessary bundle bloat.

🤖 Prompt for AI Agents
In `@package.json` around lines 22 - 33, Remove the three unused dependencies from
package.json: delete the entries for "@langchain/google-genai",
"@langchain/openai", and "@ai-sdk/langchain" (leave "@langchain/langgraph" and
"@langchain/core" intact as langgraph is used in lib/agents/map-langgraph.tsx);
after editing package.json run the package manager install (npm/yarn/pnpm) and
run the test/build to ensure nothing else imports those packages and no
transitive changes break the project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.0.0 Standard release Agent Backend Issues on schema and database Map Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants