noah-medra/prompt-builder
Composer 安装命令:
composer require noah-medra/prompt-builder
包简介
Compose and execute structured AI prompts in PHP with a fluent, framework-agnostic API (Laravel-friendly).
README 文档
README
PromptBuilder composes structured AI prompts with a fluent, query-builder-style API and executes them against an LLM (Ollama today). Its defining idea is a clean split between three concerns, so you can build a prompt once and render it however your target model prefers, in whatever language you want, and send it through whichever driver you like:
compose ─────────────▶ render ─────────────▶ execute
PromptBuilder RendererInterface PromptDriverInterface
→ PromptSpec (data) → text / chat / XML → talks to the model
(pure, no I/O) (pure, localizable) (the only layer doing I/O)
- 🧩 Composable — persona, context, instructions (
must/mustNot), few-shot examples, nested sub-instructions,{param}interpolation, conditionals. - 🖨️ Multi-format rendering — plain text, chat messages, or XML from the same spec.
- 🌍 Localizable — section labels in 6 languages (English default), pluggable.
- 🧠 Conversation memory — pluggable history stores (in-memory or Laravel cache).
- 🔌 Driver-based execution — framework-agnostic (Guzzle) or Laravel-native.
- 🧪 Framework-free core — compose and render with zero Laravel required; the Laravel bits (facade, cache history, translator bridge) are strictly opt-in.
Contents
- Requirements · Installation · Quick start
- Architecture
- Composition · Rendering · Language (i18n)
- Execution · Conversation history
- Laravel integration · Testing
Requirements
- PHP 8.1+
guzzlehttp/guzzle^7(installed automatically) for the standalone driver- Laravel is optional. When used inside an app,
illuminate/support^10 | ^11 | ^12is supported — needed only for the facade, the Laravel driver, the cache history store and the translator bridge.
Installation
composer require noah-medra/prompt-builder
This is a library:
composer.lockandvendor/are intentionally not shipped with the source. Your application resolves the dependency against its own lock.
Quick start
use NoahMedra\PromptBuilder\PromptBuilder; use NoahMedra\PromptBuilder\Drivers\OllamaDriver; $builder = PromptBuilder::make() ->persona('Tu es un professeur de mathématiques bienveillant et rigoureux.') ->context("L'élève est en terminale et prépare son bac.") ->must('Réponds en français') ->mustNot('Ne donne jamais la réponse finale sans expliquer le raisonnement') ->example('Résous x + 2 = 5', 'On isole x : x = 5 - 2 = 3.') ->withParams(['sujet' => 'les suites numériques', 'ton' => 'encourageant']) ->instruction('Adopte un ton {ton} en abordant {sujet}.') ->language('fr') // section labels in French (default: English) ->ask('Explique ce qu\'est une suite arithmétique.'); // 1. Preview the composed prompt WITHOUT any network call: echo $builder->toPrompt(); // 2. Execute it against a model: $output = $builder->driver(new OllamaDriver())->process()->getOutput(); echo $output->get('message.content');
toPrompt() above prints:
# Rôle
Tu es un professeur de mathématiques bienveillant et rigoureux.
# Contexte
L'élève est en terminale et prépare son bac.
# Exemples
Exemple 1 :
Entrée : Résous x + 2 = 5
Sortie attendue : On isole x : x = 5 - 2 = 3.
# Instructions
- [Obligatoire] Réponds en français
- [Interdit] Ne donne jamais la réponse finale sans expliquer le raisonnement
- Adopte un ton encourageant en abordant les suites numériques.
# Question
Explique ce qu'est une suite arithmétique.
A runnable, network-free demo lives in examples/basic-usage.php.
Architecture
Each layer has one job and never reaches into the next. That's what lets you unit test composition in isolation, render the same prompt differently per model, and swap execution backends.
| Layer | Classes | Responsibility |
|---|---|---|
| Compose | PromptBuilder → PromptSpec, Instructions\Instruction, Examples\Example |
Build the prompt as plain data. Pure, no I/O, no framework. |
| Render | Rendering\RendererInterface → TextRenderer, ChatMessagesRenderer, XmlRenderer |
Turn a PromptSpec into a string or chat-message array. Pure and localizable. |
| Execute | Drivers\PromptDriverInterface → Drivers\OllamaDriver, Drivers\Laravel\OllamaDriver |
Render the spec and send it to a model. The only layer that does I/O. |
PromptSpec is the hand-off: the builder fills it, a renderer reads it, a driver
sends it. You can grab it directly with getSpec() for full control.
Composition
Every method below only mutates the internal PromptSpec — no I/O, no driver.
| Method | Purpose |
|---|---|
persona(string) |
Who the model should act as |
context(string) |
Background information |
instruction(string, ?Closure) |
A neutral instruction (optionally with nested sub-instructions) |
must(string, ?Closure) |
A positive constraint, rendered with a [Required] marker |
mustNot(string, ?Closure) |
A negative constraint, rendered with a [Forbidden] marker |
example(string $input, string $output) |
A few-shot input/output pair |
expectResponseFormat(string $json) |
Ask for a specific JSON output shape (throws if the sample isn't valid JSON) |
withParams(array) / setParams(array) |
Values for {placeholder} interpolation |
language(string) / locale(string) |
Language of the rendered labels (default English) |
ask(string) |
The actual question |
when(bool, Closure $ifTrue, ?Closure $ifFalse) |
Conditional composition |
getSpec() |
Escape hatch to the raw PromptSpec |
Nested instructions
instruction(), must() and mustNot() accept a closure to add nested
sub-instructions. Each ->add() appends a sibling at the same depth; pass a
closure to add() to go one level deeper.
$builder->instruction('Structure your answer', function ($ist) { $ist->add('Start with a recap') ->add('Give a worked example') ->add('End with an exercise', callback: function ($sub) { $sub->add('Include its solution'); }); });
Parameter interpolation
Any text you pass supports {key} and {nested.key} placeholders, resolved from
withParams(). Unknown placeholders are left untouched (not silently emptied)
so typos are easy to spot.
$builder->withParams(['tone' => 'formal', 'user' => ['name' => 'Sam']]) ->instruction('Answer in a {tone} tone to {user.name}.'); // -> "Answer in a formal tone to Sam."
Conditionals
$builder->when($user->isPremium(), fn ($b) => $b->must('Include an in-depth analysis'), fn ($b) => $b->must('Keep it short'), );
Structured JSON output
$builder->expectResponseFormat('{"summary": "…", "answer": "…"}');
Adds an explicit "answer only with valid JSON matching this shape" instruction to the rendered prompt, and validates that the sample you pass is itself valid JSON (throwing otherwise).
Rendering
toPrompt() renders the composed prompt so you can iterate on quality for free.
It defaults to TextRenderer; pass any renderer to get a different shape.
use NoahMedra\PromptBuilder\Rendering\ChatMessagesRenderer; use NoahMedra\PromptBuilder\Rendering\XmlRenderer; $builder->toPrompt(); // string (default) $builder->toPrompt(new ChatMessagesRenderer()); // [['role' => 'system', ...], ['role' => 'user', ...]] $builder->toPrompt(new XmlRenderer()); // <prompt><persona>…</persona>…</prompt>
| Renderer | Output | Use for |
|---|---|---|
TextRenderer |
Single string with # Role, # Context, … sections |
Completion-style APIs, previews |
ChatMessagesRenderer |
{role, content} message array (system + history + question) |
Chat APIs (Ollama /api/chat, OpenAI, Anthropic) |
XmlRenderer |
Well-formed, escaped XML with explicit tags | Models that follow XML-delimited structure better |
Write your own by implementing Rendering\RendererInterface — it receives a
PromptSpec and returns a string or a {role, content} array.
Language (i18n)
The section labels the renderer emits (# Role, [Required], Example n:, the
JSON-output instruction, …) are localized. English is the default; pick another
language per builder:
PromptBuilder::make() ->context('...') ->must('...') ->language('fr') // or ->locale('fr') ->ask('...') ->toPrompt(); // -> "# Rôle", "# Contexte", "[Obligatoire] …", "# Question"
Bundled locales: en (default), es, fr, de, zh, ar. An unknown
locale, or a key missing in a locale, falls back to English. Only the labels are
translated — your persona/context/instruction text is emitted verbatim, and
XmlRenderer tag names stay English on purpose (they're structural).
Translation goes through a framework-free Translation\TranslatorInterface. The
default Translation\ArrayTranslator reads bundled PHP language files with no
framework. Point it at your own directory, or implement the interface for full
control:
use NoahMedra\PromptBuilder\Rendering\TextRenderer; use NoahMedra\PromptBuilder\Translation\ArrayTranslator; // Custom catalog directory, laid out as {locale}/labels.php $renderer = new TextRenderer(new ArrayTranslator('es', '/path/to/lang'));
Using Laravel's translator
Inside Laravel, the service provider registers the bundled strings under the
promptbuilder translation namespace. Publish them to customize:
php artisan vendor:publish --tag=promptbuilder-lang
Then render through Laravel's translator (which follows the app locale and any
overrides you published) with Translation\Laravel\LaravelTranslator:
use NoahMedra\PromptBuilder\Rendering\TextRenderer; use NoahMedra\PromptBuilder\Translation\Laravel\LaravelTranslator; $renderer = new TextRenderer(new LaravelTranslator(app('translator')));
Execution
$builder->driver($driver) // a PromptDriverInterface instance or class-string ->process(); // renders the PromptSpec and sends it $output = $builder->getOutput(); // ?BuilderOutput (null before process())
If you call process() without setting a driver, the standalone
Drivers\OllamaDriver is used by default.
BuilderOutput decodes JSON responses and lets you pluck values with dotted
paths, or grab the raw body:
$output->get('message.content'); // dotted-path access into decoded JSON $output->getRaw(); // the raw response string
Choosing an Ollama driver
Two interchangeable implementations ship with the package:
| Driver | Built on | Use when |
|---|---|---|
Drivers\OllamaDriver |
Guzzle directly | You want a framework-agnostic driver that works in any PHP script, no booted Laravel app. This is the default. Accepts an injectable Guzzle client for testing. |
Drivers\Laravel\OllamaDriver |
Laravel Http facade |
You're already in a Laravel app and want Http::fake() in your tests. Requires a booted application. |
Both accept model, endpoint, a renderer, and a timeoutSeconds:
use NoahMedra\PromptBuilder\Drivers\OllamaDriver; use NoahMedra\PromptBuilder\Rendering\ChatMessagesRenderer; $driver = new OllamaDriver( model: 'llama3.1', endpoint: 'http://localhost:11434/api/chat', renderer: new ChatMessagesRenderer(), timeoutSeconds: 30, );
Writing a custom driver
Implement Drivers\PromptDriverInterface. A driver receives a PromptSpec
directly (never a pre-rendered string) and picks its own renderer:
use NoahMedra\PromptBuilder\BuilderOutput; use NoahMedra\PromptBuilder\Drivers\PromptDriverInterface; use NoahMedra\PromptBuilder\PromptSpec; use NoahMedra\PromptBuilder\Rendering\ChatMessagesRenderer; class MyDriver implements PromptDriverInterface { public function process(PromptSpec $spec): BuilderOutput { $messages = (new ChatMessagesRenderer())->render($spec); // ...send $messages to your API... return new BuilderOutput($responseBody); } }
Conversation history
Call useHistory() to enable multi-turn memory. Prior turns are loaded into the
prompt, and process() appends this turn's question and the model's reply back
into the store — so the next builder using the same store sees the full exchange.
use NoahMedra\PromptBuilder\History\InMemoryHistoryStore; $store = new InMemoryHistoryStore(); // default store; works anywhere PromptBuilder::make()->useHistory($store)->driver($driver)->ask('Bonjour')->process(); PromptBuilder::make()->useHistory($store)->driver($driver)->ask('Et ensuite ?')->process(); // The second call is sent the first question + answer as chat history.
| Store | Persistence | Notes |
|---|---|---|
History\InMemoryHistoryStore |
Process lifetime | Default, framework-free |
History\Laravel\CacheHistoryStore |
Across requests, via the Laravel cache | Pass a conversation id: new CacheHistoryStore('conv-42') |
Implement History\HistoryStoreInterface (all(), push(), clear()) for a
custom backend. You can also seed turns manually with
setHistory([['role' => 'user', 'content' => '…']]).
Laravel integration
Everything above works without Laravel. When you are in a Laravel app, these
opt-in conveniences light up automatically via package auto-discovery
(PromptBuilderServiceProvider):
- Facade —
'promptbuilder'is bound in the container; eachPromptBuilder::make()returns a fresh, isolated builder. - Translations — bundled strings are registered under the
promptbuildernamespace and publishable (--tag=promptbuilder-lang). - Laravel-native pieces —
Drivers\Laravel\OllamaDriver,History\Laravel\CacheHistoryStore,Translation\Laravel\LaravelTranslator.
use NoahMedra\PromptBuilder\Facades\PromptBuilder; $prompt = PromptBuilder::make() ->context('...') ->ask('...') ->toPrompt();
Testing
composer install vendor/bin/phpunit # everything vendor/bin/phpunit --testsuite Unit # framework-free unit tests vendor/bin/phpunit --testsuite Feature # Laravel-integration tests (Testbench)
The Unit suite covers the framework-free core (composition, every renderer,
translation, history) with plain PHPUnit. The Feature suite uses Orchestra
Testbench to boot a real Laravel app for the facade, the Laravel driver (via
Http::fake()), the cache history store and the translator bridge. The
framework-agnostic Ollama driver is tested with a Guzzle MockHandler, asserting
the composed prompt is really what gets sent.
License
Released under the MIT License — see LICENCE.txt.
noah-medra/prompt-builder 适用场景与选型建议
noah-medra/prompt-builder 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 50 次下载、GitHub Stars 达 0, 最近一次更新时间为 2025 年 12 月 02 日, 在 PHP 生态内属于活跃度较高的组件。
我们在过去多个企业项目中使用过 noah-medra/prompt-builder 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 noah-medra/prompt-builder 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 noah-medra/prompt-builder 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
This module allows drag & drop grouping of items in a GridField
Adds text size controls and text-to-speech controls to Flarum discussion content.
Native Blade date, datetime, and date range pickers.
Symfony and Flysystem integration for the maintained KCFinder continuation.
Laravel integration for the maintained KCFinder continuation.
PHPStan rules shared across KnpLabs organization projects
统计信息
- 总下载量: 50
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 31
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-12-02