DEV Community

Cover image for InAppAI: Open-Source AI Agents for React Apps
Chaoming Li
Chaoming Li

Posted on

InAppAI: Open-Source AI Agents for React Apps

2026 is the year AI stops explaining what to do and starts doing it.

Users don't want walkthroughs. They want outcomes.

"Add a high-priority task to review the Q1 budget."
"Mark all completed tasks as archived."
"Cancel my subscription."

Not a help article. Just... done.

Today we're open-sourcing InAppAI: React components for building AI agents that actually operate your application.

npm install @inappai/react
Enter fullscreen mode Exit fullscreen mode

This post breaks down the three core concepts that make it work: Agents, Context, and Tools.


The Three Core Concepts

InAppAI is built around a simple mental model:

Concept What it does
Agent The AI assistant configured for your app
Context Real-time app state the agent can see
Tools Actions the agent can execute

Here's how they fit together:

User: "Add a task to buy groceries"
          ↓
    ┌─────────────┐
    │    AGENT    │  ← Understands intent
    └─────────────┘
          ↓
    ┌─────────────┐
    │   CONTEXT   │  ← Sees current todos: [{id: 1, text: "Call mom"}]
    └─────────────┘
          ↓
    ┌─────────────┐
    │    TOOLS    │  ← Calls addTodo({ text: "Buy groceries" })
    └─────────────┘
          ↓
    UI updates with new todo
Enter fullscreen mode Exit fullscreen mode

Let's look at each one.


1. Agents

An Agent is a server-side AI configuration identified by an agentId. Each agent has its own:

  • AI provider (OpenAI, Anthropic, or Google)
  • Model settings (temperature, max tokens)
  • Knowledge base configuration
  • Rate limits and caching rules

Your React component connects to an agent like this:

import { InAppAI } from '@inappai/react';

function App() {
  const [messages, setMessages] = useState<Message[]>([]);

  return (
    <InAppAI
      agentId="your-agent-id"
      messages={messages}
      onMessagesChange={setMessages}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

The agentId is the only required prop. Everything else—provider selection, failover logic, rate limiting—is handled server-side. You can switch from GPT-4 to Claude without changing your frontend code.

Why server-side?

  • API keys never touch the client
  • Change models without redeploying
  • Add rate limits per user/tenant
  • Enable knowledge base RAG without client complexity

2. Context

Context is the bridge between your UI state and the agent's understanding. Without context, the agent is blind to what's happening in your app.

Pass context as an object or a function that returns fresh state:

<InAppAI
  agentId="your-agent-id"
  messages={messages}
  onMessagesChange={setMessages}

  // Static context
  context={{ userId: "123", plan: "pro" }}

  // Or dynamic context (recommended)
  context={() => ({
    todos: todos.map(t => ({
      id: t.id,
      text: t.text,
      completed: t.completed,
      priority: t.priority,
    })),
    stats: {
      total: todos.length,
      active: todos.filter(t => !t.completed).length,
      completed: todos.filter(t => t.completed).length,
    },
  })}
/>
Enter fullscreen mode Exit fullscreen mode

Why use a function?

When the agent executes a tool (like adding a todo), the component automatically calls your context function again to get fresh state. This lets the agent see the results of its actions:

1. User: "Add a task to buy milk"
2. Agent calls addTodo tool
3. Tool handler: setTodos([...todos, newTodo])
4. Component calls context() again  ← Fresh state
5. Agent sees updated todo list
6. Agent responds: "Done! I've added 'Buy milk' to your list."
Enter fullscreen mode Exit fullscreen mode

Without fresh context, the agent would still see the old state and couldn't confirm what changed.

What to include in context

Include anything the agent needs to make decisions:

context={() => ({
  // Current page/view
  currentView: 'dashboard',

  // User info
  user: {
    name: user.name,
    role: user.role,
    permissions: user.permissions,
  },

  // Relevant data
  selectedItems: selectedIds,
  filters: activeFilters,

  // Application state
  isLoading: false,
  lastError: null,
})}
Enter fullscreen mode Exit fullscreen mode

Keep it lean. Don't dump your entire Redux store—just what's relevant for the agent's decisions.


3. Tools

Tools are functions the agent can call to take action in your app. Each tool has:

  • name: Identifier the agent uses to call it
  • description: Helps the agent decide when to use it
  • parameters: JSON Schema defining expected inputs
  • handler: The function that actually runs (client-side)

Here's a complete example:

import { Tool } from '@inappai/react';

const todoTools: Tool[] = [
  {
    name: 'addTodo',
    description: 'Create a new todo item when the user wants to add a task',
    parameters: {
      type: 'object',
      properties: {
        text: {
          type: 'string',
          description: 'The task description',
        },
        priority: {
          type: 'string',
          enum: ['low', 'medium', 'high'],
          description: 'Task priority level. Default to medium if not specified.',
        },
      },
      required: ['text'],
    },
    handler: async ({ text, priority = 'medium' }) => {
      const newTodo = {
        id: crypto.randomUUID(),
        text,
        completed: false,
        priority,
      };
      setTodos(prev => [...prev, newTodo]);
      return { success: true, todo: newTodo };
    },
  },

  {
    name: 'completeTodo',
    description: 'Mark a todo as completed by its ID or by matching text',
    parameters: {
      type: 'object',
      properties: {
        identifier: {
          type: 'string',
          description: 'The todo ID or a keyword from the task text',
        },
      },
      required: ['identifier'],
    },
    handler: async ({ identifier }) => {
      const todo = todos.find(
        t => t.id === identifier ||
             t.text.toLowerCase().includes(identifier.toLowerCase())
      );

      if (!todo) {
        return { success: false, error: 'Todo not found' };
      }

      setTodos(prev =>
        prev.map(t => t.id === todo.id ? { ...t, completed: true } : t)
      );

      return { success: true, completed: todo.text };
    },
  },

  {
    name: 'deleteTodo',
    description: 'Remove a todo from the list permanently',
    parameters: {
      type: 'object',
      properties: {
        id: {
          type: 'string',
          description: 'The todo ID to delete',
        },
      },
      required: ['id'],
    },
    handler: async ({ id }) => {
      const todo = todos.find(t => t.id === id);
      if (!todo) {
        return { success: false, error: 'Todo not found' };
      }

      setTodos(prev => prev.filter(t => t.id !== id));
      return { success: true, deleted: todo.text };
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

How tool execution works

  1. User sends message → "Add a high-priority task to review the budget"
  2. Agent analyzes intent → Decides to call addTodo
  3. Backend returns tool call{ name: "addTodo", arguments: { text: "Review the budget", priority: "high" } }
  4. Handler executes → Your React state updates
  5. Results sent back → Agent sees { success: true, todo: {...} }
  6. Agent responds → "I've added 'Review the budget' as a high-priority task."

The handler runs in your React component, so it has full access to state setters, hooks, and any other client-side logic.

Tool design tips

Be specific with descriptions. The agent uses descriptions to decide which tool to call:

// ❌ Vague
description: 'Handles todos'

// ✅ Specific
description: 'Create a new todo item when the user wants to add a task, reminder, or action item to their list'
Enter fullscreen mode Exit fullscreen mode

Return useful information. The agent uses return values to formulate its response:

// ❌ Minimal
return { success: true };

// ✅ Informative
return {
  success: true,
  todo: newTodo,
  message: `Created "${newTodo.text}" with ${newTodo.priority} priority`
};
Enter fullscreen mode Exit fullscreen mode

Handle errors gracefully. Return error info instead of throwing:

handler: async ({ id }) => {
  const item = items.find(i => i.id === id);
  if (!item) {
    return { success: false, error: `Item with ID ${id} not found` };
  }
  // ... proceed
}
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

Here's a complete example combining all three concepts:

import { useState } from 'react';
import { InAppAI, Tool, Message } from '@inappai/react';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
}

export function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [messages, setMessages] = useState<Message[]>([]);

  // Define tools with access to state
  const tools: Tool[] = [
    {
      name: 'addTodo',
      description: 'Create a new todo item',
      parameters: {
        type: 'object',
        properties: {
          text: { type: 'string', description: 'Task description' },
          priority: {
            type: 'string',
            enum: ['low', 'medium', 'high'],
            description: 'Priority level'
          },
        },
        required: ['text'],
      },
      handler: async ({ text, priority = 'medium' }) => {
        const newTodo: Todo = {
          id: crypto.randomUUID(),
          text,
          completed: false,
          priority,
        };
        setTodos(prev => [...prev, newTodo]);
        return { success: true, todo: newTodo };
      },
    },
    {
      name: 'listTodos',
      description: 'Get all todos, optionally filtered by status',
      parameters: {
        type: 'object',
        properties: {
          filter: {
            type: 'string',
            enum: ['all', 'active', 'completed'],
            description: 'Filter by completion status',
          },
        },
      },
      handler: async ({ filter = 'all' }) => {
        let filtered = todos;
        if (filter === 'active') filtered = todos.filter(t => !t.completed);
        if (filter === 'completed') filtered = todos.filter(t => t.completed);
        return { todos: filtered, count: filtered.length };
      },
    },
    // ... more tools
  ];

  return (
    <div className="app">
      {/* Your existing UI */}
      <TodoList todos={todos} />

      {/* AI Agent */}
      <InAppAI
        agentId="your-agent-id"
        messages={messages}
        onMessagesChange={setMessages}
        tools={tools}
        context={() => ({
          todos: todos.map(({ id, text, completed, priority }) => ({
            id, text, completed, priority,
          })),
          stats: {
            total: todos.length,
            active: todos.filter(t => !t.completed).length,
            completed: todos.filter(t => t.completed).length,
          },
        })}
        displayMode="popup"
        theme="light"
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now users can say:

  • "Add three tasks: buy milk, call dentist, and finish report"
  • "What tasks do I have left?"
  • "Mark the dentist task as done"
  • "Delete all completed tasks"

The agent understands intent, sees your current state, and executes the right tools.


What's Next

This is just the foundation. InAppAI also supports:

  • Knowledge Base RAG — Ground responses in your docs
  • Multiple display modes — Popup, sidebar, inline, fullscreen
  • Theme customization — 7 built-in themes or bring your own
  • Tool Registry — Manage tools across components with namespaces

We'll cover these in future posts.


Get Started

Try the demo: react-demo.inappai.com

Read the docs: inappai.com/docs

Star on GitHub: github.com/inappai/react

InAppAI is MIT-licensed. Inspect the code, fork it, extend it, or contribute back.


Questions? Drop a comment below or open an issue on GitHub.

Top comments (0)