Asia/Dhaka
Projects

Intent-Driven AI Agent for Project Management Platform

image
August 1, 2025
This is an Intent-Driven AI Agent built for a Project Management Platform. It sits between the user and the entire system — frontend, backend, and database — acting as an intelligent command router. The user speaks naturally; the agent figures out what they want, where that resource lives, and how to act on it, then dispatches structured commands to the right layer. Unlike generic AI assistants that return free-form text, this agent always outputs a typed JSON action object that the frontend consumes deterministically. No hallucinated responses, no ambiguous instructions — just clean, structured commands.
The agent operates on a strict 3-phase pipeline:
Natural Language → Intent Detection → Action Dispatch
It never guesses freely. Every action it can take is mapped to a pre-defined intent + resource contract, making it safe, predictable, and auditable.
Architecture Overview — the full system at a glance: user → agent core → dispatch → systems → back to user

Every user request resolves to a structured intent in the form resource:action. The full registry is finite and typed — the agent cannot invent new intents, which tightly bounds its behavior and eliminates entire classes of unexpected or unauthorized actions.
ResourceActions
documentread, write, create, delete
projectread, create, update, delete
subprojectread, create, update
teamread, add_member, remove_member
routenavigate
chapterread, edit
Intent Registry — the finite, typed resource:action taxonomy that bounds all agent behavior

The user sends a natural language message via the frontend chat interface. Example input: "Edit the introduction chapter of the Q3 Report" The agent's LLM core classifies the request and extracts:
  • Intent: document:write
  • Resource type: document
  • Target: Q3 Report → Chapter: Introduction
  • Operation: edit
Before acting, the agent resolves which resource is being referred to. It queries the backend/DB to:
  • Identify the document by name match
  • Locate the specific chapter within it
  • Fetch the current chapter content and metadata (doc ID, chapter ID, version)
Based on the resolved intent type, one of two dispatch paths is taken: Backend/DB path (e.g., document:write): The agent takes the fetched chapter content, processes the requested edit through an LLM edit processor (rewrite, insert, or replace), and returns a structured Action Response to the frontend containing the intent, resource ID, edited content, and a diff of the changes. Frontend path (e.g., route:navigate): The agent constructs a Navigation Command — a JSON instruction like { intent: "route:navigate", path: "/projects/42/documents/7" } — and the frontend handles the actual navigation itself. The agent never touches the DOM. Every response from the agent is a typed JSON object that the frontend consumes:
Json
{
  "intent": "document:write",
  "resource": "chapter",
  "resourceId": "ch_intro_q3report",
  "action": "edit",
  "payload": {
    "original": "...",
    "edited": "...",
    "diff": [...]
  },
  "status": "success"
}
The frontend renders based on this contract — it never parses or interprets free text.
document:write Detailed Trace — a concrete worked example walking through all 6 stages from user input to rendered result

The LLM core is prompted with the finite intent registry as a constraint. Given the user's message, it outputs a structured classification — not a response. This turns the LLM from a text generator into a precise classifier operating within defined boundaries. Named entity resolution is the hardest part of any conversational agent. When a user says "the Q3 Report," the resolver queries the backend to find the matching document, disambiguating by project context, recency, and user ownership. This step transforms vague human references into concrete resource IDs. A simple but critical piece: it reads the resource field of the resolved intent object and routes to the correct handler — backend handler for data operations, frontend handler for navigation and UI commands. Clean separation, zero ambiguity. The two handler types enforce a strict separation of concerns:
  • Backend handler — owns all data mutations and reads. Communicates with the database and returns structured payloads.
  • Frontend handler — emits JSON navigation/UI commands that the frontend framework interprets. The agent never directly manipulates the DOM or application state.

  • Separation of concerns: The agent reasons, the frontend renders, the backend stores. Clean contracts between all three layers.
  • Bounded behavior: The finite intent registry means the agent can never do something unexpected or unauthorized. The possibility space is mathematically closed.
  • Full auditability: Every action maps to a logged intent + resourceId pair — complete traceability for compliance and debugging.
  • Extensible by design: Adding a new capability means adding one intent to the registry and one handler. No changes to the core pipeline.
  • Framework-agnostic: Navigation commands are pure JSON instructions — the agent works with any frontend framework that can consume them.
  • No free-text ambiguity: The frontend never has to parse the agent's response. It's always a typed contract.

When a project has 200 documents and a user says "the report," the context resolver needs a ranked disambiguation strategy. I implemented a scoring system combining: exact name match weight, project context (current project the user is in), recency (last-edited timestamp), and ownership (docs the user created). The top-scored match is used, and low-confidence resolutions surface a clarification prompt rather than guessing. LLMs naturally want to be helpful beyond their defined scope. The prompt engineering for the intent detector required careful system prompt construction: providing the full intent registry as the only valid output space, using few-shot examples of correctly classified requests, and including an explicit intent: "unknown" fallback for requests outside the registry — which triggers a graceful "I can't help with that" response rather than a hallucinated action. For document:write operations, the response payload includes a structural diff so the frontend can render a change-tracking view. I implemented a word-level diff algorithm that produces a list of { type: "unchanged" | "added" | "removed", text: string } objects — lightweight enough to serialize in JSON and expressive enough to drive a rich diff UI on the frontend.
Hey! Got questions?Ask me anything 👋