Files
nibiru-framework.com/application/module/ai/plugins/ollama.php
stephan 48c839d927 Initial public push: docs cosmos v4 + AI module + framework groundwork
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>
2026-05-08 15:22:18 +02:00

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.');
}
}