jvjvjv / code-talker
Multi-provider AI communications package for Laravel — chatbots, streaming, tool-use, memory, and admin management.
Requires
- php: ^8.3
- guzzlehttp/guzzle: ^7.0
- inertiajs/inertia-laravel: ^2.0 || ^3.0
- laravel/ai: ^0.9
- laravel/framework: ^12.62 || ^13.15
- laravel/mcp: ^0.8.1
- symfony/dom-crawler: ^7.4 || ^8.1
Requires (Dev)
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.0
Suggests
- bspdx/keystone: Suggested for host-app authorization around admin AI routes and related access control wiring.
README
Multi-provider AI communications package for Laravel — chatbots, streaming, tool-use, memory, and admin management.
Requirements
- PHP ^8.3
- Laravel ^12.62 || ^13.15
Provider communication runs on Laravel's first-party laravel/ai
SDK, installed as a dependency. You do not need to publish or configure
config/ai.php — provider credentials come from AiSystem database records and
are bridged into laravel/ai providers at runtime. (Publish it only if your app
uses laravel/ai on its own.)
Installation
composer require jvjvjv/code-talker
The package installs the Laravel Inertia adapter as a runtime dependency because its public and admin controllers render Inertia responses.
Publish the config and migrations, then run them:
php artisan vendor:publish --tag=code-talker-config php artisan vendor:publish --tag=code-talker-migrations php artisan migrate
If you want to customize the package route files directly in the host app, publish them too:
php artisan vendor:publish --tag=code-talker-routes
Upgrading
After upgrading the package, re-publish or reconcile your published config:
php artisan vendor:publish --tag=code-talker-config --force
This matters because the package merges its config shallowly (Laravel's
mergeConfigFrom). If your app already has a published config/code-talker.php
with a providers key, newer or corrected nested keys the package ships —
most notably providers.*.base_url, the raw_exchanges block, and the
conversations block — are not backfilled into it. Your previously published array is used as-is, so a stale
publish can silently keep an outdated provider base URL (see
Troubleshooting).
After --force, re-apply any local customizations. If you'd rather not
overwrite, diff your file against vendor/jvjvjv/code-talker/config/code-talker.php
and copy over only the new keys.
Configuration
config/code-talker.php controls package-wide behavior:
| Key | Default | Description |
|---|---|---|
user_model |
App\Models\User::class |
Eloquent model used for authenticated users |
middleware |
['web'] |
Middleware applied to public chat routes |
admin_middleware |
['web', 'auth', 'can:manage-ai-tools'] |
Middleware applied to admin routes |
reserved_slugs |
[] |
Additional slugs that cannot be used for root-path chatbots |
schedule |
true |
Set to false to disable the package's automatic scheduled jobs |
conversations.idle_timeout_minutes |
30 |
Inactivity before a conversation is marked Completed |
Suggested host-app packages
bspdx/keystoneis suggested if you want a ready-made host-app authorization layer for the package's admin AI routes.
Provider environment variables
API keys, models, and token limits live on AiSystem database records — not in
env vars. The env vars below only supply fallback base URLs (used when an
AiSystem has no base_url), the Anthropic API version, and the LM Studio
server URL:
ANTHROPIC_API_VERSION=2023-06-01
ANTHROPIC_BASE_URL=https://api.anthropic.com/v1
OPENAI_BASE_URL=https://api.openai.com/v1
GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
GROK_BASE_URL=https://api.x.ai/v1
LMSTUDIO_SERVER_URL=http://localhost:1234
The providers.*.pricing config keys feed conversation usage/cost tracking.
Base URLs must include the API version path segment — /v1 for
anthropic, openai, and grok; /v1beta for gemini (the defaults above
already do). Provider communication treats the configured URL as the complete
base and appends the endpoint directly. This differs from the retired
anthropic-ai/sdk, which accepted a bare host (https://api.anthropic.com) and
appended /v1 itself — so a bare host now produces 404s. This applies both to a
live AiSystem.base_url value and to these providers.*.base_url fallbacks. See
Troubleshooting.
Raw Provider Exchange Logging
Every laravel/ai HTTP request/response can be captured verbatim into the
ai_provider_exchanges table for debugging and auditing.
'raw_exchanges' => [ 'enabled' => env('CODE_TALKER_RAW_EXCHANGES_ENABLED', true), 'providers' => env('CODE_TALKER_RAW_EXCHANGES_PROVIDERS', 'lm-studio'), 'retention_days' => (int) env('CODE_TALKER_RAW_EXCHANGES_RETENTION_DAYS', 14), ],
enabled— master switch for capture.providers— comma-separated allow-list ofAiSystemprovider values (lm-studio,anthropic,openai,openai-compatible,gemini,grok), orallto capture every provider. Defaults tolm-studio.retention_days— rows older than this are removed byphp artisan ai:prune-provider-exchanges, scheduled daily at 03:00 (respects thescheduleflag).
Request bodies and response bytes are stored, but request headers are never recorded, so provider API keys are not persisted.
Troubleshooting
Provider is unavailable: HTTP request returned status code 404 — returned
by the model-status / readiness check (and cloud-provider chat also 404s) for
anthropic, openai, gemini, or grok.
- Cause: the provider base URL is missing its version segment — e.g.
https://api.anthropic.cominstead ofhttps://api.anthropic.com/v1. This is usually a stale published config (the shallow merge described in Upgrading never backfilled the corrected default) or an oldAiSystem.base_url/ANTHROPIC_BASE_URLvalue carried over from theanthropic-ai/sdkera. - Fix: set the
AiSystem.base_urlto include/v1(or clear it to fall back to the config default), fix any bare-host*_BASE_URLenv var, and re-publish the config with--force(see Upgrading). The correct URL forms are listed under Provider environment variables.
AI Systems
An AiSystem record represents a fully configured provider endpoint. Create one through the admin UI at /admin/ai/systems or via a seeder. Key fields:
| Field | Description |
|---|---|
provider |
One of: anthropic, openai, openai-compatible, gemini, grok, lm-studio |
model |
Provider-specific model name |
api_key |
Stored encrypted |
max_tokens |
Maximum output tokens per request |
temperature |
Sampling temperature (overrides bot-level default) |
context_length |
Context window for local models (LM Studio) |
enable_thinking |
Enable extended thinking / reasoning output (Anthropic) |
allowed_tools |
Array of tool names the model may invoke |
system_prompt_id |
Optional FK to an AiSystemPrompt record |
is_active |
Inactive systems are rejected by the factory |
Getting an agent in code
AgentFactory bridges an AiSystem record into a configured
laravel/ai agent:
use Jvjvjv\CodeTalker\Services\LaravelAi\AgentFactory; use Jvjvjv\CodeTalker\Models\AiSystem; // From a specific system record $agent = app(AgentFactory::class)->forSystem( AiSystem::find($id), instructions: 'You are a helpful assistant.', maxTokens: 2048, temperature: 0.7, ); // From a feature key (resolves the default system for that feature) $agent = app(AgentFactory::class)->forFeature('my-feature'); $response = $agent->prompt('Hello!'); // Laravel\Ai\Responses\AgentResponse echo $response->text; foreach ($agent->stream('Hello!') as $event) { // Laravel\Ai\Streaming\Events\* (TextDelta, ToolCall, StreamEnd, ...) }
The agent runs on laravel/ai, so everything from the
laravel/ai documentation — streaming,
tool use, structured output — applies. Prior versions returned an
AiClientContract from AiClientFactory; both were removed in 0.6.0.
Feature defaults
Map a feature key to a default AiSystem via the ai_system_feature_defaults table (managed through /admin/ai/systems). This decouples application code from specific system IDs.
Chat Bots
An AiChatBot defines a user-facing persona. Create one at /admin/ai/chat-bots. Key fields:
| Field | Description |
|---|---|
ai_system_id |
The backing AiSystem |
name |
Display name |
slug |
URL-safe identifier, must be unique |
access_path |
chat → /chat/{slug}, root → /{slug} |
prompt_template |
System prompt with optional placeholders (see below) |
require_visitor_identity |
Prompt anonymous visitors for name and email |
tools_enabled |
Whether the bot may invoke registered tools |
temperature |
Overrides AiSystem temperature for this bot |
Chatbot authentication and authorization are not managed by this package. The consuming application must decide which users or guests can reach chatbot routes by applying its own middleware, gates, or policies around the package routes.
Prompt template placeholders
These tokens are replaced when a conversation starts:
| Placeholder | Value |
|---|---|
{{bot_name}} |
Bot's display name |
{{bot_slug}} |
Bot's slug |
{{bot_description}} |
Bot's description field |
{{visitor_name}} |
Name collected from anonymous visitor (if any) |
{{visitor_email}} |
Email collected from anonymous visitor (if any) |
The final system prompt is assembled as: AiSystemPrompt.content + prompt template + ## Learned Insights (injected memories).
Auto-registered routes
All routes use the middleware from code-talker.middleware.
If routes/codetalker-chatbots.php or routes/codetalker-admin.php exists in
the host app, the package will load those published copies instead of its
internal defaults.
The package does not treat any bot as inherently public or private. If some
chatbot routes should require authentication or further authorization, enforce
that entirely in the consuming application by changing code-talker.middleware
or wrapping the package routes in your own authorization layer.
| Route | Description |
|---|---|
GET /chats |
List of available bots (Inertia: ai/ChatBotsIndex) |
GET /chats/statuses |
JSON model-readiness status for all bots |
GET /chat/{slug} |
Chat UI for a bot (Inertia: ai/ChatBot) |
GET /chat/{slug}/new |
Start a new conversation |
GET /chat/{slug}/status |
JSON readiness status for one bot |
POST /chat/{slug}/warmup |
Warm up the model |
POST /chat/{slug}/messages |
Send a message (SSE stream) |
POST /chat/{slug}/reset |
Clear current conversation |
POST /chat/{slug}/switch |
Switch to a different conversation from history |
GET /chat/{slug}/{hash} |
Load a conversation by its shareable hash |
Root-access-path bots duplicate the above at /{slug} instead of /chat/{slug}.
Conversation state
The browser's active and historical conversations are stored in a session key and a 180-day encrypted cookie (ai_chat_bot_conversations_{id}). Conversations are also shareable via /chat/{slug}/{hash}.
Tool Registration
Tools are laravel/mcp Tool classes. The same
class runs in the local chat loop and can be exposed to external MCP clients
(see External MCP Server). Extend Laravel\Mcp\Server\Tool:
use Illuminate\Contracts\JsonSchema\JsonSchema; use Jvjvjv\CodeTalker\Support\ToolContext; use Laravel\Mcp\Request; use Laravel\Mcp\Response; use Laravel\Mcp\ResponseFactory; use Laravel\Mcp\Server\Attributes\Description; use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; #[Name('get-weather')] #[Description('Returns current weather for a given city.')] class GetWeatherTool extends Tool { public function __construct( private ToolContext $context, ) {} public function schema(JsonSchema $schema): array { return [ 'city' => $schema->string()->description('City name')->required(), ]; } public function handle(Request $request): Response|ResponseFactory { // ... fetch weather ... return Response::structured(['temperature' => '72°F', 'condition' => 'Sunny']); // Use Response::error('...') to signal a failure to the model. } }
Notes:
- The tool name defaults to the kebab-cased class basename (
GetWeatherTool→get-weather-tool), so set an explicit#[Name('get-weather')]for a clean name. - Inject
ToolContextfor the current user/conversation rather than depending onAiConversationdirectly. In the local chat loop it carries the live conversation ($context->conversation), deriveduserId,visitorEmail, andfeature. When the tool is called by an external MCP client it carries the authenticated user's id with no conversation, so guard conversation-only logic.
Register the directory containing your tools in AppServiceProvider:
use Jvjvjv\CodeTalker\CodeTalkerServiceProvider; public function register(): void { CodeTalkerServiceProvider::addToolDirectory( app_path('Services/Mcp/Tools'), 'App\\Services\\Mcp\\Tools\\' ); }
Tools are auto-discovered from registered directories. The AiSystem::allowed_tools
array controls which discovered tools are exposed to the model for a given bot, by
tool name.
Upgrading from a previous version: the old
AiToolHandlerContract(name()/description()/schema(): array/handle(array): array) is deprecated but still discovered and dispatched for backward compatibility. Migrate toLaravel\Mcp\Server\Toolas shown above. The built-in tools were also renamed from snake_case to kebab-case — run the published migration to update any persistedallowed_toolsvalues.
Built-in tools
The package includes built-in tools under src/Services/Mcp/Tools/ChatBot.
fetch-web-page: Fetches readable text from a URL.scan-memories: Searches stored user memories for relevant context.search-web: Searches Bing, Google, DuckDuckGo, and Brave, then returns structured results plus markdown links/snippets.
To enable the web-search tool for a system, include search-web in AiSystem::allowed_tools.
search-web input schema (high level):
query(required): Search query text.engines(optional): Any subset ofbing,google,duckduckgo,brave.page(optional): Page number for continued searching.per_engine_limit(optional): Max results per engine (1-10).
search-web response includes:
- Per-engine results with
title,url, anddescription. markdowncontaining clickable links and snippets.next_page_inputto continue searching on the next page.- Guidance for asking the model to inspect a specific link in depth.
Injecting extra dependencies into tools
If your tools need objects that aren't in the service container by default (e.g., a service scoped to the current conversation), register a parameter resolver:
CodeTalkerServiceProvider::registerToolParameterResolver( fn (AiConversation $conversation): array => [ 'myService' => app(MyService::class)->forConversation($conversation), ] );
The resolver is called once per ChatBotToolRegistry instantiation, and its return values are passed as makeWith() overrides when tools are resolved from the container.
External MCP Server
Because tools are laravel/mcp Tool classes, the same tools can be exposed to
external MCP clients (Claude Desktop, Grok, etc.) through a bundled
CodeTalkerServer. This is disabled by default. Enable it under the
code-talker.mcp config key:
'mcp' => [ 'enabled' => env('CODE_TALKER_MCP_ENABLED', false), 'web' => [ 'enabled' => true, 'path' => env('CODE_TALKER_MCP_PATH', 'mcp/code-talker'), 'middleware' => ['auth:sanctum'], ], 'local' => [ 'enabled' => false, 'handle' => env('CODE_TALKER_MCP_LOCAL_HANDLE', 'code-talker'), ], ],
- web registers an HTTP MCP endpoint via
Mcp::web(). Protect it with authentication middleware (Sanctum or OAuth per the laravel/mcp docs). The authenticated user is mapped to aToolContextso user-scoped tools such asscan-memoriesresolve the correct identity. Since there is no conversation in this context,scan-memoriessearches across all of the user's memories rather than a single feature. scan-memoriesimplementsshouldRegister()and is therefore only advertised to callers that have a user identity — anonymous callers never see it. If you expose the server on a public route, give it optional authentication middleware (authenticate when a token is present without rejecting guests) so authenticated callers still get the memory tool while anonymous callers get the stateless tools.- local registers a stdio server via
Mcp::local(), runnable through thephp artisan mcp:start {handle}command for local AI assistant integrations.
The server requires laravel/mcp, which is installed as a dependency. See the Laravel MCP documentation for client configuration, authentication, and the MCP Inspector.
Memory System
After each completed conversation, ProcessAiMemoryJob dispatches and calls AiMemoryService::processCompletedConversation(). The service sends the conversation to the same AiSystem for analysis and extracts structured memory operations (add / update / remove).
When memory extraction runs
A conversation is never explicitly "ended" — the browser simply stops sending
messages. Completion is therefore inferred by the
ai:complete-idle-conversations command, scheduled every 15 minutes:
conversation goes quiet
│
├─ idle_timeout_minutes elapse (default 30)
│
▼
ai:complete-idle-conversations → status = Completed
│
▼
AiConversationObserver → ProcessAiMemoryJob (once per conversation)
Memories appear up to idle_timeout_minutes + 15 after a chat ends. Lower
conversations.idle_timeout_minutes for faster extraction at the cost of
splitting conversations where the user pauses mid-chat.
Extraction runs once per conversation, not once per message —
analyzeConversation() sends the entire transcript on every call, so per-turn
extraction would cost O(N²) tokens in conversation length. If you disable the
package scheduler ('schedule' => false), register the command yourself or
memory extraction will never run.
Memories are stored in AiFeatureMemory and scoped per user:
- Authenticated users: scoped by
user_id - Anonymous visitors: scoped by
visitor_email(requiresrequire_visitor_identity = true)
Memory categories
| Category | Description |
|---|---|
preference |
How the user likes things done |
domain_knowledge |
Facts about the user not covered by other data |
system_tuning |
What worked well or poorly in this bot's approach |
Memories are ranked by confidence and times_reinforced and injected into the system prompt under ## Learned Insights. Memories can be reviewed and edited at /admin/ai/memories.
To rebuild all memories for a feature from historical conversations:
# Via admin UI at /admin/ai/memories — use the "Rebuild" action # Or via code: app(\Jvjvjv\CodeTalker\Services\AiMemoryService::class)->rebuildMemories('chat-bot:my-bot');
Admin Routes
The admin route group is registered under /admin/ai/* and uses the middleware
defined in code-talker.admin_middleware, which defaults to ['web', 'auth', 'can:manage-ai-tools'].
If your host app does not already provide that gate, wire it yourself or change
code-talker.admin_middleware to the authorization middleware your application
already uses.
All admin routes are under /admin/ai and require the can:manage-ai-tools gate (configurable via admin_middleware).
| Prefix | Resource |
|---|---|
/admin/ai/systems |
AI Systems CRUD + interaction log viewer |
/admin/ai/system-prompts |
Reusable system prompt CRUD |
/admin/ai/chat-bots |
Chat bot CRUD |
/admin/ai/conversations |
Conversation viewer + usage backfill trigger |
/admin/ai/memories |
Feature memory CRUD + per-feature rebuild |
Define the manage-ai-tools gate in your AppServiceProvider or AuthServiceProvider:
Gate::define('manage-ai-tools', fn ($user) => (bool) $user->is_admin);
Scheduled Jobs
The package registers four jobs automatically (requires Laravel's scheduler to be running):
| Job | Schedule | Description |
|---|---|---|
ai:sync-conversation-usage |
Twice daily (00:00, 12:00) | Syncs token counts and cost to AiConversation |
BackfillConversationUsageJob |
Daily at 02:30 | Backfills usage for conversations missing cost data |
ai:prune-provider-exchanges |
Daily at 03:00 | Removes ai_provider_exchanges rows past retention |
ai:complete-idle-conversations |
Every 15 minutes | Completes idle conversations, triggering memory extract |
Disable automatic scheduling in config and register manually if needed:
// config/code-talker.php 'schedule' => false, // Your app's Console\Kernel or routes/console.php Schedule::command('ai:sync-conversation-usage')->twiceDaily();
Artisan Commands
# Detect and store capability flags for all AiSystem records php artisan ai:backfill-system-capabilities # Backfill usage/cost data for conversations missing it php artisan ai:backfill-conversation-usage # Sync current token/cost totals to ai_conversations php artisan ai:sync-conversation-usage # Delete ai_provider_exchanges rows older than raw_exchanges.retention_days php artisan ai:prune-provider-exchanges # Mark idle conversations Completed, triggering memory extraction php artisan ai:complete-idle-conversations php artisan ai:complete-idle-conversations --minutes=60 --dry-run