This is the snapshot the production landing site (nibiru-framework.com) is deployed from. Brings together the recent splash + docs migration to the v4 "Cosmos" design system, the new in-framework AI module, and the framework groundwork that backs the framework-reference extraction. What lands: - docs/: Astro + Starlight site with the v4 dark cosmic palette, GalaxyHero canvas constellation, Mission Control chat (wired to /api/oracle → api.neuronetz.ai via providers.mjs Ollama), 5-panel MMVC stage (Model · AI · Module · Controller · View), translated EN/DE/JA/ES/FR content, PWA + sitemap + llms.txt + Umami analytics. - docs/design-system/: canonical mockup bundle (source/index-v2.html for splash, source/docs-system.html + preview/ for docs, SPEC.md, tokens). - docs/scripts/extraction/framework-reference-v2.md: deep framework reference (~1.6k lines, file:line citations, every public factory and idiom — basis for the LoRA training corpus. - application/module/ai/: AI module with chat / embed / RAG / agent plugins, plus pdoQuery / httpGet / fileRead tools and Modelfile + smoke-test in training/. - application/module/users/: user / ACL / form-factory traits used as the reference plugin pattern for the framework docs. - application/settings/config/database/: schema + seed migrations including the AI module tables (200–203). - Form factory + autogenerator changes the framework-reference-v2 covers. Production secrets stay out: docs/.env, settings.production.ini and ai.production.ini are all gitignored (.example files are in tree). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
173 lines
5.4 KiB
PHP
173 lines
5.4 KiB
PHP
<?php
|
|
namespace Nibiru\Module\Ai\Plugins;
|
|
|
|
/**
|
|
* Ollama HTTP transport. The lowest layer of the AI stack — every other
|
|
* plugin (Chat, Embed, RAG, Agent) ultimately speaks through this.
|
|
*
|
|
* Works against any Ollama-compatible endpoint (localhost:11434, a
|
|
* remote Ollama via reverse proxy, vLLM with the /api/chat shim,
|
|
* LiteLLM proxies, etc.).
|
|
*/
|
|
class Ollama
|
|
{
|
|
protected string $baseUrl;
|
|
protected int $timeout;
|
|
protected int $retries;
|
|
|
|
public function __construct(\stdClass $cfg)
|
|
{
|
|
$this->baseUrl = rtrim((string) ($cfg->ollama_base_url ?? 'http://localhost:11434'), '/');
|
|
$this->timeout = (int) ($cfg->ollama_timeout ?? 90);
|
|
$this->retries = (int) ($cfg->ollama_retries ?? 1);
|
|
}
|
|
|
|
/**
|
|
* POST /api/chat — synchronous, non-streaming.
|
|
*
|
|
* @param string $model e.g. "nibiru-coder:1.0"
|
|
* @param array $messages [{role: 'user'|'assistant'|'system', content: '...'}]
|
|
* @param array $options {temperature, num_predict, top_p, ...}
|
|
* @return array decoded JSON response
|
|
*/
|
|
public function chat(string $model, array $messages, array $options = []): array
|
|
{
|
|
return $this->post('/api/chat', [
|
|
'model' => $model,
|
|
'messages' => $messages,
|
|
'stream' => false,
|
|
'options' => $options,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* POST /api/embeddings — single input.
|
|
*
|
|
* @return array{embedding: float[]}
|
|
*/
|
|
public function embed(string $model, string $prompt): array
|
|
{
|
|
return $this->post('/api/embeddings', [
|
|
'model' => $model,
|
|
'prompt' => $prompt,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* POST /api/generate — single-prompt completion (no chat structure).
|
|
*/
|
|
public function generate(string $model, string $prompt, array $options = []): array
|
|
{
|
|
return $this->post('/api/generate', [
|
|
'model' => $model,
|
|
'prompt' => $prompt,
|
|
'stream' => false,
|
|
'options' => $options,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* GET /api/tags — list available models.
|
|
*/
|
|
public function listModels(): array
|
|
{
|
|
return $this->get('/api/tags');
|
|
}
|
|
|
|
/**
|
|
* GET /api/ps — list currently-loaded models.
|
|
*/
|
|
public function listLoaded(): array
|
|
{
|
|
return $this->get('/api/ps');
|
|
}
|
|
|
|
/**
|
|
* POST /api/pull — pull a model onto the Ollama server.
|
|
*/
|
|
public function pull(string $name): array
|
|
{
|
|
return $this->post('/api/pull', ['name' => $name, 'stream' => false]);
|
|
}
|
|
|
|
/**
|
|
* POST /api/create — register a custom model from a Modelfile.
|
|
*/
|
|
public function create(string $name, string $modelfile): array
|
|
{
|
|
return $this->post('/api/create', [
|
|
'name' => $name,
|
|
'modelfile' => $modelfile,
|
|
'stream' => false,
|
|
]);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// HTTP helpers
|
|
// -----------------------------------------------------------------------
|
|
|
|
protected function post(string $path, array $body): array
|
|
{
|
|
$url = $this->baseUrl . $path;
|
|
$payload = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
return $this->withRetries(function () use ($url, $payload) {
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => $this->timeout,
|
|
CURLOPT_CONNECTTIMEOUT => 10,
|
|
]);
|
|
$body = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$err = curl_error($ch);
|
|
curl_close($ch);
|
|
if ($body === false) {
|
|
throw new \RuntimeException("Ollama transport error: $err");
|
|
}
|
|
if ($code >= 400) {
|
|
$snip = substr((string) $body, 0, 240);
|
|
throw new \RuntimeException("Ollama HTTP $code: $snip");
|
|
}
|
|
$decoded = json_decode((string) $body, true);
|
|
if (!is_array($decoded)) {
|
|
throw new \RuntimeException('Ollama returned non-JSON body.');
|
|
}
|
|
return $decoded;
|
|
});
|
|
}
|
|
|
|
protected function get(string $path): array
|
|
{
|
|
$ch = curl_init($this->baseUrl . $path);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 15,
|
|
CURLOPT_CONNECTTIMEOUT => 5,
|
|
]);
|
|
$body = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
if ($body === false || $code >= 400) {
|
|
throw new \RuntimeException("Ollama GET $path failed (HTTP $code)");
|
|
}
|
|
return json_decode((string) $body, true) ?? [];
|
|
}
|
|
|
|
protected function withRetries(callable $fn)
|
|
{
|
|
$last = null;
|
|
for ($i = 0; $i <= $this->retries; $i++) {
|
|
try {
|
|
return $fn();
|
|
} catch (\RuntimeException $e) {
|
|
$last = $e;
|
|
if ($i < $this->retries) usleep(250_000 * (1 << $i)); // 250ms, 500ms, ...
|
|
}
|
|
}
|
|
throw $last ?? new \RuntimeException('Ollama call failed.');
|
|
}
|
|
}
|