Botovis

Architecture

Deep dive into the Botovis ReAct agent loop, monorepo structure, and internal systems

Botovis is built as a modular system using the ReAct (Reasoning + Acting) agent pattern. This page explains the internal architecture, package structure, request lifecycle, and extension points.

Package Structure

Botovis is a monorepo with four packages, each with a single responsibility:

botovis/
├── packages/
│   ├── core/        → botovis/core (Framework-agnostic PHP)
│   ├── laravel/     → botovis/botovis-laravel (Laravel integration)
│   ├── widget/      → @botovis/widget (TypeScript Web Component)
│   └── telegram/    → botovis/botovis-telegram (Telegram bot adapter)

Core Package

The core package contains all contracts (interfaces), DTOs, enums, schema models, the agent loop, and orchestration logic. It has zero framework dependencies — only PHP 8.1+ is required.

core/src/
├── Contracts/        # 8 interfaces (LlmDriver, SchemaDiscovery, ActionExecutor, etc.)
├── DTO/              # Data objects (LlmResponse, SecurityContext, Conversation, Message, AuthorizationResult)
├── Enums/            # ActionType, ColumnType, IntentType, RelationType
├── Schema/           # DatabaseSchema, TableSchema, ColumnSchema, RelationSchema
├── Tools/            # ToolInterface, ToolRegistry, ToolResult
├── Intent/           # IntentResolver, ResolvedIntent (simple mode)
├── Conversation/     # ConversationState
├── Agent/            # AgentLoop, AgentOrchestrator, AgentResponse, AgentState, AgentStep, StreamingEvent
├── Orchestrator.php
└── OrchestratorResponse.php

Laravel Package

Implements core interfaces using Laravel's Eloquent, Cache, Session, Auth, and other services:

laravel/src/
├── BotovisServiceProvider.php       # DI bindings, publishes, routes, Blade directive
├── Http/
│   ├── BotovisController.php        # Chat + SSE streaming endpoints
│   └── ConversationController.php   # Conversation CRUD
├── Llm/
│   ├── LlmDriverFactory.php         # Creates driver from config
│   ├── AnthropicDriver.php          # Claude API with native tool calling
│   ├── OpenAiDriver.php             # OpenAI API (+ compatible endpoints)
│   └── OllamaDriver.php            # Local Ollama
├── Tools/
│   ├── BaseTool.php                 # Shared Eloquent helpers
│   ├── SearchRecordsTool.php        # search_records
│   ├── CountRecordsTool.php         # count_records
│   ├── GetSampleDataTool.php        # get_sample_data
│   ├── GetColumnStatsTool.php       # get_column_stats
│   ├── AggregateTool.php            # aggregate
│   ├── CreateRecordTool.php         # create_record
│   ├── UpdateRecordTool.php         # update_record
│   └── DeleteRecordTool.php         # delete_record
├── Schema/
│   └── EloquentSchemaDiscovery.php  # Model reflection + DB introspection
├── Security/
│   └── BotovisAuthorizer.php        # Role-based authorization
├── Conversation/
│   └── CacheConversationManager.php # Cache-based state
├── Repositories/
│   ├── EloquentConversationRepository.php  # Persistent storage
│   └── SessionConversationRepository.php   # Session-based storage
├── Models/
│   ├── BotovisConversation.php
│   └── BotovisMessage.php
└── Commands/
    ├── ModelsCommand.php            # botovis:models
    ├── DiscoverCommand.php          # botovis:discover
    └── ChatCommand.php              # botovis:chat

Widget Package

Zero-dependency chat UI built as a Web Component with Shadow DOM:

widget/src/
├── index.ts          # Custom element registration, exports
├── botovis-chat.ts   # Main component (1600+ lines)
├── api.ts            # REST + SSE client with CSRF handling
├── types.ts          # All TypeScript interfaces
├── styles.ts         # Complete CSS (Shadow DOM adopted stylesheets)
├── i18n.ts           # Turkish + English translations (68 keys)
└── icons.ts          # 30+ inline SVG icons

Built as ES module, UMD, and IIFE formats via Vite.

The ReAct Agent Loop

The core of Botovis is the ReAct (Reasoning + Acting) pattern:

User Query → Think → Act → Observe → (repeat) → Final Answer

How It Works

  1. Think — The LLM analyzes the user's query and the conversation history, decides what to do
  2. Act — The LLM calls one or more tools (e.g., search_records, count_records)
  3. Observe — The tool results are fed back to the LLM as observations
  4. Repeat — If the LLM needs more information, it calls more tools
  5. Answer — Once the LLM has enough information, it produces a natural language response

Request Lifecycle

Browser                Widget               Laravel              AgentLoop
  │                      │                     │                     │
  │  Click send          │                     │                     │
  │─────────────────────>│                     │                     │
  │                      │  POST /stream       │                     │
  │                      │────────────────────>│                     │
  │                      │                     │  Build SecurityCtx  │
  │                      │                     │  Resolve user role  │
  │                      │                     │  Get conv. history  │
  │                      │                     │────────────────────>│
  │                      │                     │                     │  Build system prompt
  │                      │                     │                     │  (schema + rules + tools)
  │                      │                     │                     │
  │                      │                     │                     │  LOOP:
  │                      │                     │                     │  ┌───────────────────────┐
  │                      │                     │                     │  │ LLM.chatWithTools()   │
  │  SSE: step           │                     │  yield step event   │  │                       │
  │<─────────────────────│<────────────────────│<────────────────────│  │ → text? → complete    │
  │                      │                     │                     │  │ → tool? → execute     │
  │  SSE: step (w/obs)   │                     │  yield step event   │  │   read: run+observe   │
  │<─────────────────────│<────────────────────│<────────────────────│  │   write: pause+confirm│
  │                      │                     │                     │  └───────────────────────┘
  │                      │                     │                     │
  │  SSE: message        │                     │  yield message      │
  │<─────────────────────│<────────────────────│<────────────────────│
  │  SSE: done           │                     │                     │
  │<─────────────────────│<────────────────────│                     │

Confirmation Flow

When the agent detects a write operation:

Agent detects write tool (create_record, update_record, delete_record)

    ├── Adds tool_call message to state (with thought)
    ├── Adds [PENDING] tool_result placeholder
    ├── Yields SSE confirmation event to the widget
    └── Pauses loop (AgentState = needs_confirmation)

User clicks Confirm

    ├── POST /stream-confirm
    ├── Execute the write tool
    ├── Replace [PENDING] with actual result
    ├── Resume agent loop (so LLM can summarize the result)
    └── Yield message + done events

User clicks Reject

    ├── POST /reject
    ├── Agent receives rejection
    └── Responds with cancellation message

Agent Internals

Single Step Processing

// 1. Build prompt with schema, tools, permissions, urgency warnings
$systemPrompt = $this->buildSystemPrompt($state);

// 2. Generate stopping: remove tools on last step
$toolDefs = $stepsRemaining <= 1 ? [] : $this->tools->toFunctionDefinitions();

// 3. Call LLM with native tool calling
$response = $this->llm->chatWithTools($systemPrompt, $messages, $toolDefs);

// 4. Handle response
if ($response->isText())      complete with answer
if ($response->isToolCall())  handleToolCalls()

Parallel Tool Calls

When the LLM returns multiple tool calls in a single response:

// 1. Add ALL tool_call messages first (API requires balanced pairs)
foreach ($toolCalls as $tc) {
    $state->addToolCallMessage($tc['id'], $tc['name'], $tc['params']);
}

// 2. Process each: read → execute, write → [PENDING]
foreach ($toolCalls as $tc) {
    if ($tool->requiresConfirmation()) {
        $state->addToolResultMessage($tc['id'], '[PENDING]...');
    } else {
        $result = $this->tools->execute($tc['name'], $tc['params']);
        $state->addToolResultMessage($tc['id'], $result->toObservation());
    }
}

// 3. All results count as ONE step
$step = AgentStep::action($stepNum, $thought, $toolNames, $observations);

Generate Stopping

Prevents the "max steps reached with no answer" scenario:

  1. Urgency warnings — At ≤3 steps remaining, the system prompt includes a WARNING telling the agent to wrap up
  2. Critical message — At ≤1 step remaining, a CRITICAL message forces the agent to answer immediately
  3. Tool removal — On the very last step, tools are removed from the API call entirely, forcing a text-only response

LLM Driver Architecture

All drivers implement LlmDriverInterface:

interface LlmDriverInterface
{
    public function chat(string $systemPrompt, array $messages): string;
    public function chatWithTools(string $systemPrompt, array $messages, array $tools): LlmResponse;
    public function stream(string $systemPrompt, array $messages, callable $onToken): string;
    public function name(): string;
}

Normalized Message Format

Internally, Botovis uses a provider-independent message format:

// Tool call (from LLM)
['role' => 'assistant', 'content' => 'thought...', 'tool_call' => [
    'id' => 'call_123',
    'name' => 'count_records',
    'params' => ['table' => 'products'],
]]

// Tool result (observation)
['role' => 'tool_result', 'tool_call_id' => 'call_123', 'content' => 'Count: 247']

Each driver's convertMessages() method translates this to the API-specific format:

  • Anthropic — Content blocks with tool_use / tool_result types, merges consecutive same-role messages
  • OpenAItool_calls array on assistant messages, separate tool role messages
  • Ollama — Similar to OpenAI with synthetic tool call IDs

Schema Discovery

EloquentSchemaDiscovery builds the database schema by combining:

  1. Eloquent reflection — Reads fillable/guarded, casts, relationships from model classes
  2. Database introspection — Queries column types, nullable, defaults, max length, enum values
  3. Convention-based discovery — Detects enum values from static methods (statusOptions(), statusLabels(), etc.)
  4. Label generation — Converts model names to display labels (e.g., ProductCategory → "Product Categories")

The resulting DatabaseSchema is:

  • Sent to the LLM as system prompt context
  • Filtered by user permissions before display
  • Used by tools to validate table names and columns

Service Container Bindings

The BotovisServiceProvider registers these singletons:

InterfaceDefault Implementation
SchemaDiscoveryInterfaceEloquentSchemaDiscovery
LlmDriverInterfaceLlmDriverFactory::make()
ActionExecutorInterfaceEloquentActionExecutor
ConversationManagerInterfaceCacheConversationManager
ConversationRepositoryInterfaceEloquentConversationRepository
BotovisAuthorizerBotovisAuthorizer
ToolRegistryConfigured with 8 built-in tools
OrchestratorSimple mode orchestrator
AgentOrchestratorAgent mode orchestrator

Extension Points

Botovis is designed to be extensible. You can replace almost any component:

Custom LLM Driver

use Botovis\Core\Contracts\LlmDriverInterface;

class MyCustomDriver implements LlmDriverInterface
{
    // Implement all methods...
}

// In a service provider:
$this->app->singleton(LlmDriverInterface::class, fn() => new MyCustomDriver());

Custom Tools

$this->app->afterResolving(ToolRegistry::class, function ($registry) {
    $registry->register(new MyCustomTool());
});

See the Tools page for details on creating custom tools.

Custom Authorizer

use Botovis\Core\Contracts\AuthorizerInterface;

$this->app->singleton(AuthorizerInterface::class, fn() => new MyCustomAuthorizer());

Custom Conversation Storage

use Botovis\Core\Contracts\ConversationRepositoryInterface;

$this->app->singleton(ConversationRepositoryInterface::class, fn() => new RedisConversationRepository());

Asset Publishing

TagFilesDestination
botovis-configconfig/botovis.phpconfig/
botovis-assetsWidget JS bundlepublic/vendor/botovis/
botovis-viewsBlade templatesresources/views/vendor/botovis/
botovis-migrationsMigration filesdatabase/migrations/

On this page