Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38f9eeac0f | |||
| f7ec4e59bf | |||
| d5894c7bd4 | |||
| d5b4d339ce | |||
| 782a48cee2 | |||
| dc8cab7813 | |||
| e595d8e328 | |||
| e0acb69524 | |||
| 05660a4e01 | |||
| 481c0c209f | |||
| 84a23e6b14 |
11 changed files with 526 additions and 49 deletions
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
git fetch --depth=1 origin "$GITHUB_SHA"
|
||||
git checkout -q FETCH_HEAD
|
||||
|
||||
# The fai-module-sdk dependency is a git dep against a repo
|
||||
# The chain-module-sdk dependency is a git dep against a repo
|
||||
# that lives in another Forgejo org. The repo-scoped
|
||||
# GITHUB_TOKEN cannot read it, and the Forgejo instance has
|
||||
# REQUIRE_SIGNIN_VIEW=true so anonymous read is also denied.
|
||||
|
|
|
|||
223
.forgejo/workflows/sign.yml
Normal file
223
.forgejo/workflows/sign.yml
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# F∆I module bundle signing workflow (cosign key-pinning model).
|
||||
#
|
||||
# Drop-in template for any module repo under chain-modules/.
|
||||
# Triggers on tag push (vX.Y.Z) — builds the module, packs the
|
||||
# .fai bundle, signs the bundle bytes with the Flemming.AI
|
||||
# signing key (ECDSA P-256), and attaches bundle + .sig sidecar
|
||||
# to the matching Forgejo Release.
|
||||
#
|
||||
# The hub-side verifier ships in 0.12.0+: when an operator sets
|
||||
# `require_signatures: true`, install downloads the .sig sidecar
|
||||
# from `<wasm_url>.sig` and verifies it against the pinned store
|
||||
# public key (built-in for `bundled` / `official`, per-store
|
||||
# config for everything else).
|
||||
#
|
||||
# Adopt by copying this file to `.forgejo/workflows/sign.yml` in
|
||||
# the module repo and replacing the MODULE_NAME placeholder.
|
||||
# Add the signing key to Forgejo secrets as FAI_SIGNING_KEY (PEM,
|
||||
# unencrypted, ECDSA P-256). Rotation is a 5-minute key-roll +
|
||||
# binary re-release; see infra/cosign/README.md.
|
||||
|
||||
name: sign-bundle
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_TOOLCHAIN: "1.86"
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
||||
|
||||
jobs:
|
||||
sign:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # attach signature to release
|
||||
|
||||
env:
|
||||
# The canonical module name as written in module.yaml.
|
||||
# Used in the bundle filename + wasm path.
|
||||
MODULE_NAME: llm-chat
|
||||
|
||||
steps:
|
||||
# See fai/platform CI for background on the manual external
|
||||
# checkout: the DinD runner cannot resolve forgejo:3000, so
|
||||
# we clone via the external URL with $GITHUB_TOKEN.
|
||||
- name: Checkout via external URL
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p "$GITHUB_WORKSPACE"
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
git init -q
|
||||
git remote add origin \
|
||||
"https://x-access-token:${GITHUB_TOKEN}@git.flemming.ai/${GITHUB_REPOSITORY}.git"
|
||||
git fetch --depth=1 origin "$GITHUB_SHA"
|
||||
git checkout -q FETCH_HEAD
|
||||
|
||||
# The chain-module-sdk dependency lives in another Forgejo org
|
||||
# behind REQUIRE_SIGNIN_VIEW. MODULE_SDK_PAT (org-level
|
||||
# secret, read-only on fai/module-sdk) authenticates cargo's
|
||||
# git fetch via insteadOf.
|
||||
- name: Configure git URL rewrite for SDK fetch
|
||||
env:
|
||||
SDK_PAT: ${{ secrets.MODULE_SDK_PAT }}
|
||||
run: |
|
||||
git config --global \
|
||||
"url.https://x-access-token:${SDK_PAT}@git.flemming.ai/.insteadOf" \
|
||||
"https://git.flemming.ai/"
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
git \
|
||||
jq \
|
||||
openssl
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y \
|
||||
--profile minimal \
|
||||
--default-toolchain "$RUST_TOOLCHAIN" \
|
||||
--target wasm32-wasip2
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Build module (wasm32-wasip2)
|
||||
run: cargo build --release --target wasm32-wasip2
|
||||
|
||||
- name: Install fai CLI
|
||||
# Pinned to the production channel; signing requires the
|
||||
# `fai pack` command. Override FAI_VERSION when bundle
|
||||
# format compatibility matters.
|
||||
run: |
|
||||
curl -fsSL https://get.chain.flemming.ai | sh -s -- --no-bootstrap
|
||||
# The installer drops fai at $HOME/.local/bin
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fai --version
|
||||
|
||||
- name: Stage bundle inputs
|
||||
# The wasm artefact is named after the Cargo package
|
||||
# (underscored), which may differ from the module name
|
||||
# in module.yaml (dashed). Resolve it from Cargo.toml so
|
||||
# the template stays agnostic to the project's naming
|
||||
# convention.
|
||||
run: |
|
||||
set -eu
|
||||
CARGO_NAME=$(grep -E '^name *= *"' Cargo.toml \
|
||||
| head -1 | sed 's/.*"\(.*\)".*/\1/')
|
||||
mkdir -p staging
|
||||
cp module.yaml staging/
|
||||
cp "target/wasm32-wasip2/release/${CARGO_NAME}.wasm" staging/module.wasm
|
||||
if [ -f sbom.cdx.json ]; then cp sbom.cdx.json staging/; fi
|
||||
if [ -f MODULE.md ]; then cp MODULE.md staging/; fi
|
||||
if [ -f MODULE.de.md ]; then cp MODULE.de.md staging/; fi
|
||||
|
||||
- name: Pack bundle
|
||||
id: pack
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
BUNDLE="${{ env.MODULE_NAME }}-${{ github.ref_name }}.fai"
|
||||
fai pack staging --output "$BUNDLE"
|
||||
echo "bundle=$BUNDLE" >> "$GITHUB_OUTPUT"
|
||||
ls -la "$BUNDLE"
|
||||
|
||||
- name: Sign bundle (Flemming.AI signing key)
|
||||
env:
|
||||
FAI_SIGNING_KEY_PEM: ${{ secrets.FAI_SIGNING_KEY }}
|
||||
run: |
|
||||
if [ -z "${FAI_SIGNING_KEY_PEM:-}" ]; then
|
||||
cat >&2 <<'EOF'
|
||||
|
||||
FAI_SIGNING_KEY secret is empty or undefined.
|
||||
|
||||
This workflow signs module bundles with the Flemming.AI
|
||||
module-signing key (ECDSA P-256, PKCS#8 PEM, unencrypted).
|
||||
The matching public key is pinned in the fai-hub binary at
|
||||
infra/cosign/official.pub — hubs that require_signatures
|
||||
will reject anything not signed with this key.
|
||||
|
||||
To configure:
|
||||
1. Forgejo -> chain-modules org -> Settings -> Secrets
|
||||
2. Add secret FAI_SIGNING_KEY (org-level recommended)
|
||||
3. Paste the unencrypted PEM (BEGIN PRIVATE KEY ...)
|
||||
The encrypted source-of-truth lives on Stefan's Mac at
|
||||
~/.fai-secrets/flemming-ai-signing.key (passphrase in
|
||||
mSecure under "Ch∆In — Module Signing Key (Private)").
|
||||
|
||||
See fai/platform infra/cosign/OPERATOR-HANDOFF.md for the
|
||||
full setup runbook and rotation process.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
umask 077
|
||||
echo "$FAI_SIGNING_KEY_PEM" > /tmp/signing-key.pem
|
||||
openssl dgst -sha256 \
|
||||
-sign /tmp/signing-key.pem \
|
||||
-out "${{ steps.pack.outputs.bundle }}.sig.raw" \
|
||||
"${{ steps.pack.outputs.bundle }}"
|
||||
base64 < "${{ steps.pack.outputs.bundle }}.sig.raw" \
|
||||
> "${{ steps.pack.outputs.bundle }}.sig"
|
||||
rm "${{ steps.pack.outputs.bundle }}.sig.raw"
|
||||
shred -u /tmp/signing-key.pem || rm -f /tmp/signing-key.pem
|
||||
ls -la "${{ steps.pack.outputs.bundle }}.sig"
|
||||
|
||||
- name: Round-trip verify
|
||||
# Sanity-check: the freshly-signed bundle must verify
|
||||
# against the well-known public key before we publish it.
|
||||
run: |
|
||||
curl -fsSL https://git.flemming.ai/fai/platform/raw/branch/main/infra/cosign/official.pub \
|
||||
-o /tmp/official.pub
|
||||
openssl dgst -sha256 \
|
||||
-verify /tmp/official.pub \
|
||||
-signature <(base64 -d < "${{ steps.pack.outputs.bundle }}.sig") \
|
||||
"${{ steps.pack.outputs.bundle }}"
|
||||
|
||||
- name: Attach bundle + signature to Forgejo Release
|
||||
run: |
|
||||
set -eu
|
||||
BUNDLE="${{ steps.pack.outputs.bundle }}"
|
||||
SIG="$BUNDLE.sig"
|
||||
REL_JSON=$(curl -sS \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
"https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}")
|
||||
REL_ID=$(echo "$REL_JSON" | jq -r '.id // empty')
|
||||
if [ -z "$REL_ID" ] || [ "$REL_ID" = "null" ]; then
|
||||
REL_ID=$(curl -sS -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases" \
|
||||
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"draft\":false,\"prerelease\":false,\"body\":\"Auto-generated by sign-bundle workflow\"}" \
|
||||
| jq -r '.id')
|
||||
fi
|
||||
upload_one() {
|
||||
local asset_path="$1" asset
|
||||
asset=$(basename "$asset_path")
|
||||
EXISTING=$(curl -sS \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
"https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets" \
|
||||
| jq -r ".[] | select(.name==\"${asset}\") | .id")
|
||||
if [ -n "$EXISTING" ]; then
|
||||
curl -sS -X DELETE \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
"https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets/${EXISTING}" \
|
||||
|| true
|
||||
fi
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-F "attachment=@${asset_path}" \
|
||||
"https://git.flemming.ai/api/v1/repos/${GITHUB_REPOSITORY}/releases/${REL_ID}/assets?name=${asset}" \
|
||||
> /dev/null
|
||||
echo "uploaded $asset"
|
||||
}
|
||||
upload_one "$BUNDLE"
|
||||
upload_one "$SIG"
|
||||
echo "release ${GITHUB_REF_NAME} ready: https://git.flemming.ai/${GITHUB_REPOSITORY}/releases/tag/${GITHUB_REF_NAME}"
|
||||
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -39,17 +39,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "fai-module-sdk"
|
||||
name = "chain-module-sdk"
|
||||
version = "0.1.2"
|
||||
source = "git+https://git.flemming.ws/fai/module-sdk.git?branch=main#f209fbc86f531b53e576f3cdbdf75750eef3b07a"
|
||||
source = "git+https://git.flemming.ai/fai/chain-module-sdk-rust.git?branch=main#98e98e9371f7409560a1ef08bc0923d9a2506449"
|
||||
dependencies = [
|
||||
"fai-module-sdk-macros",
|
||||
"chain-module-sdk-macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
|
|
@ -57,15 +51,21 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "fai-module-sdk-macros"
|
||||
name = "chain-module-sdk-macros"
|
||||
version = "0.1.2"
|
||||
source = "git+https://git.flemming.ws/fai/module-sdk.git?branch=main#f209fbc86f531b53e576f3cdbdf75750eef3b07a"
|
||||
source = "git+https://git.flemming.ai/fai/chain-module-sdk-rust.git?branch=main#98e98e9371f7409560a1ef08bc0923d9a2506449"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
|
|
@ -140,7 +140,7 @@ checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545"
|
|||
name = "llm_chat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fai-module-sdk",
|
||||
"chain-module-sdk",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ description = "F∆I module providing llm.chat"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
fai-module-sdk = { git = "https://git.flemming.ws/fai/module-sdk.git", branch = "main" }
|
||||
chain-module-sdk = { git = "https://git.flemming.ai/fai/chain-module-sdk-rust.git", branch = "main" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -58,7 +58,7 @@ APPENDIX: How to apply the Apache License to your work.
|
|||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright 2026 fai-modules
|
||||
Copyright 2026 Flemming.AI
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
|||
106
MODULE.de.md
Normal file
106
MODULE.de.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# llm.chat
|
||||
|
||||
Generischer Ollama-kompatibler LLM-Chat-Adapter. Das
|
||||
Baustein-Modul für jeden Flow, der eine LLM-Einzelantwort
|
||||
braucht — klassifizieren, Felder extrahieren, umformulieren,
|
||||
entscheiden — ohne einen eigenen HTTP-Client zu bauen.
|
||||
|
||||
## Capability
|
||||
|
||||
- `llm.chat@0.1.0`
|
||||
|
||||
## Eingaben
|
||||
|
||||
| Name | Typ | Beschreibung |
|
||||
| ---------------- | ---- | ------------------------------------------------------------------------- |
|
||||
| `prompt` | text | Der User-Prompt. |
|
||||
| `endpoint` | text | Ollama-förmiger `/api/chat`-URL (z.B. `http://localhost:11434/api/chat`). |
|
||||
| `model` | text | Modell-ID (z.B. `qwen2.5:14b`, `llama3.1:8b`). |
|
||||
| `api_key` | text | Optionaler Bearer-Token für Cloud-Endpunkte. |
|
||||
| `system_prompt` | text | Optionale System-Nachricht. Leer = Default des Modells. |
|
||||
|
||||
## Ausgaben
|
||||
|
||||
| Name | Typ | Beschreibung |
|
||||
| ---------------- | ---- | --------------------------------------------------------------------- |
|
||||
| `response` | text | Die Antwort des Assistants als Plain-Text. |
|
||||
| `model_endpoint` | text | URL, gegen die die Antwort erzeugt wurde. |
|
||||
| `model_name` | text | Modell-ID wie an die LLM-API gesendet. |
|
||||
| `model_digest` | text | SHA-256-Digest des bedienenden Ollama-Modells. Leer bei Cloud-APIs. |
|
||||
|
||||
Zusammen beantworten die drei `model_*`-Ausgaben die Audit-
|
||||
Frage: „Welches genaue Modell hat diese Antwort erzeugt?" —
|
||||
diese Spur ist der Grund, warum Flows dieses Modul nutzen statt
|
||||
eigene HTTP-Calls zu sprechen.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
- "net: localhost"
|
||||
- "net: 127.0.0.1"
|
||||
- "net: api.openai.com"
|
||||
- "net: api.anthropic.com"
|
||||
```
|
||||
|
||||
Loopback (lokales Ollama) per Default. Cloud-Endpunkte brauchen
|
||||
einen Operator-Policy-Override in
|
||||
`~/.chain/config.yaml#security.max_permissions`.
|
||||
|
||||
## Warum dieses Modul statt Inline-HTTP
|
||||
|
||||
Drei Gründe:
|
||||
|
||||
1. **Audit.** Jeder LLM-Aufruf erzeugt `model_endpoint`,
|
||||
`model_name`, `model_digest` als separate Ausgaben, die
|
||||
im hash-verketteten Audit-Log neben der Response landen.
|
||||
Eine Aufsichts-Reproduzierbarkeits-Prüfung vergleicht
|
||||
einfach das Digest-Feld mit dem Modell-Snapshot.
|
||||
2. **Permission-Posture.** Operator:innen sehen `llm.chat`
|
||||
in den installierten Modulen + dessen deklarierte
|
||||
`net:`-Liste. Ein hausgemachtes HTTP-Modul würde seine
|
||||
Endpunkte entweder verstecken oder bei jeder Installation
|
||||
eine frische Review-Oberfläche schaffen.
|
||||
3. **Endpunkt-Portabilität.** Endpunkt + Modell sind Flow-
|
||||
Inputs, keine Compile-Time-Konstanten. Derselbe Flow läuft
|
||||
in der Entwicklung gegen `localhost:11434` und in Produktion
|
||||
gegen ein Inferenz-Cluster — nur die Eingabe wechselt.
|
||||
|
||||
## Grenzen in v0.1.0
|
||||
|
||||
- Kein Streaming. Die ganze Antwort wird gepuffert, bevor der
|
||||
Output-Step feuert.
|
||||
- Keine Tool-Call- / Function-Call-Oberfläche. Ein Flow, der
|
||||
Tool-Use braucht, kombiniert mehrere `llm.chat`-Schritte mit
|
||||
Prompt-Engineering oder nutzt MCP via Bridge.
|
||||
- Cloud-Provider-Adapter für OpenAI und Anthropic kommen erst,
|
||||
wenn ein Flow sie braucht. Heute ist das Ollama-Wire-Format
|
||||
das einzige Ziel.
|
||||
|
||||
## Beispiel-Flow
|
||||
|
||||
```yaml
|
||||
name: classify-incoming
|
||||
inputs:
|
||||
text: text
|
||||
steps:
|
||||
- id: classify
|
||||
use: llm.chat@^0
|
||||
with:
|
||||
prompt: $inputs.text
|
||||
system_prompt: |
|
||||
Klassifiziere den Text als: question, complaint,
|
||||
feedback, spam. Antworte nur mit dem Label.
|
||||
endpoint: "http://localhost:11434/api/chat"
|
||||
model: "qwen2.5:14b"
|
||||
outputs:
|
||||
category: $classify.response
|
||||
audit_model: $classify.model_digest
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cargo build --release --target wasm32-wasip2
|
||||
# Ausgabe: target/wasm32-wasip2/release/llm_chat.wasm
|
||||
```
|
||||
104
MODULE.md
Normal file
104
MODULE.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# llm.chat
|
||||
|
||||
Generic Ollama-compatible LLM chat adapter. The building-block
|
||||
module any flow uses when it needs a one-shot LLM completion —
|
||||
classify, extract-fields, rewrite, decide — instead of rolling
|
||||
its own HTTP client.
|
||||
|
||||
## Capability
|
||||
|
||||
- `llm.chat@0.1.0`
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---------------- | ---- | --------------------------------------------------------------- |
|
||||
| `prompt` | text | The user-facing prompt. |
|
||||
| `endpoint` | text | Ollama-shaped `/api/chat` URL (e.g. `http://localhost:11434/api/chat`). |
|
||||
| `model` | text | Model identifier (e.g. `qwen2.5:14b`, `llama3.1:8b`). |
|
||||
| `api_key` | text | Optional bearer token for cloud-hosted endpoints. |
|
||||
| `system_prompt` | text | Optional system message. Empty = use the model's default. |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---------------- | ---- | ------------------------------------------------------------------- |
|
||||
| `response` | text | The assistant's plain-text reply. |
|
||||
| `model_endpoint` | text | The endpoint the response was generated against. |
|
||||
| `model_name` | text | The model identifier as supplied to the LLM API. |
|
||||
| `model_digest` | text | SHA-256 digest of the served Ollama model. Empty for cloud APIs. |
|
||||
|
||||
Together the three `model_*` outputs answer the audit
|
||||
question: "which exact model produced this response?" — that
|
||||
audit trail is the reason flows use this module rather than
|
||||
spawning their own HTTP calls.
|
||||
|
||||
## Permissions
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
- "net: localhost"
|
||||
- "net: 127.0.0.1"
|
||||
- "net: api.openai.com"
|
||||
- "net: api.anthropic.com"
|
||||
```
|
||||
|
||||
Loopback (local Ollama) by default. Cloud endpoints require an
|
||||
operator-policy override in `~/.chain/config.yaml#security.max_permissions`.
|
||||
|
||||
## Why this module instead of inline HTTP
|
||||
|
||||
Three reasons:
|
||||
|
||||
1. **Audit.** Every LLM call surfaces `model_endpoint`,
|
||||
`model_name`, `model_digest` as separate outputs that land
|
||||
in the hash-chained audit log alongside the response. A
|
||||
regulator-facing reproducibility check just compares the
|
||||
digest field to the model snapshot.
|
||||
2. **Permission posture.** The operator sees `llm.chat` in
|
||||
the installed modules + its declared `net:` list. A
|
||||
home-grown HTTP module would either hide its endpoints
|
||||
or be a fresh review surface every time.
|
||||
3. **Endpoint portability.** The endpoint + model are flow
|
||||
inputs, not compile-time constants. The same flow runs
|
||||
against `localhost:11434` in dev and a production
|
||||
inference cluster in prod just by swapping the input.
|
||||
|
||||
## Limits in v0.1.0
|
||||
|
||||
- No streaming. The whole reply is buffered before the output
|
||||
step fires.
|
||||
- No tool-call / function-call surface. A flow needing tool
|
||||
use composes multiple `llm.chat` steps with prompt
|
||||
engineering, or uses MCP via the bridge.
|
||||
- Cloud-provider adapters for OpenAI and Anthropic are
|
||||
deferred until a flow actually needs them. Today the
|
||||
Ollama wire-format is the only target.
|
||||
|
||||
## Example flow
|
||||
|
||||
```yaml
|
||||
name: classify-incoming
|
||||
inputs:
|
||||
text: text
|
||||
steps:
|
||||
- id: classify
|
||||
use: llm.chat@^0
|
||||
with:
|
||||
prompt: $inputs.text
|
||||
system_prompt: |
|
||||
Classify the text as one of: question, complaint,
|
||||
feedback, spam. Answer with the label only.
|
||||
endpoint: "http://localhost:11434/api/chat"
|
||||
model: "qwen2.5:14b"
|
||||
outputs:
|
||||
category: $classify.response
|
||||
audit_model: $classify.model_digest
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cargo build --release --target wasm32-wasip2
|
||||
# Output: target/wasm32-wasip2/release/llm_chat.wasm
|
||||
```
|
||||
|
|
@ -29,7 +29,7 @@ on every successful invocation. Together they answer the audit
|
|||
question "which exact model produced this response?" The digest
|
||||
probe targets Ollama's `/api/show` and is best-effort — non-
|
||||
Ollama endpoints and transient probe failures yield an empty
|
||||
digest rather than blocking the response. Cf. F∆I Platform
|
||||
digest rather than blocking the response. Cf. Ch∆In
|
||||
`docs/advanced/compliance-gaps.md` Gap 1.
|
||||
|
||||
## Build
|
||||
|
|
@ -47,7 +47,7 @@ cargo build --release --target wasm32-wasip2
|
|||
|
||||
## SDK source
|
||||
|
||||
The Cargo.toml git-deps `fai-module-sdk` from
|
||||
The Cargo.toml git-deps `chain-module-sdk` from
|
||||
`https://git.flemming.ws/fai/module-sdk.git`. The Forgejo
|
||||
instance has `REQUIRE_SIGNIN_VIEW=true`, so anonymous clones
|
||||
fail; CI uses the `MODULE_SDK_PAT` actions secret + `git config
|
||||
|
|
@ -58,5 +58,5 @@ transparently. Local dev uses the same pattern via env vars.
|
|||
|
||||
Apache-2.0.
|
||||
|
||||
Author: Dr. Stefan Flemming, Flemming.AI <platform@flemming.ai>
|
||||
Repository: https://git.flemming.ws/fai-modules/llm-chat
|
||||
Author: Dr. Stefan Flemming, Flemming.AI <chain@flemming.ai>
|
||||
Repository: https://git.flemming.ws/chain-modules/llm-chat
|
||||
|
|
|
|||
BIN
module.wasm
Normal file
BIN
module.wasm
Normal file
Binary file not shown.
102
module.yaml
102
module.yaml
|
|
@ -1,4 +1,5 @@
|
|||
schema_version: 1
|
||||
schema_version: 3
|
||||
provider: chain
|
||||
name: llm-chat
|
||||
version: 0.1.0
|
||||
|
||||
|
|
@ -7,38 +8,81 @@ provides:
|
|||
- capability: llm.chat
|
||||
version: 0.1.0
|
||||
|
||||
# Inputs the invoke function accepts.
|
||||
# Declared inputs in verbose v3 form. Studio renders the
|
||||
# description in the active locale as a tooltip on each
|
||||
# port; the editor falls back to the English description
|
||||
# when no peer is configured.
|
||||
inputs:
|
||||
# The user-facing prompt text.
|
||||
prompt: text
|
||||
# Ollama-shaped /api/chat endpoint, e.g.
|
||||
# "http://localhost:11434/api/chat".
|
||||
endpoint: text
|
||||
# Model identifier as registered with the endpoint, e.g.
|
||||
# "qwen2.5:14b" or "llama3.1:8b".
|
||||
model: text
|
||||
# Optional bearer token for cloud-hosted endpoints. Empty
|
||||
# string means no Authorization header is sent.
|
||||
api_key: text
|
||||
# Optional system prompt. Empty string omits the system
|
||||
# message and lets the model use its built-in default.
|
||||
system_prompt: text
|
||||
prompt:
|
||||
type: text
|
||||
description:
|
||||
en: The user-facing prompt text the model should respond to.
|
||||
de: Der vom Modell zu beantwortende Prompt-Text.
|
||||
endpoint:
|
||||
type: text
|
||||
description:
|
||||
en: |
|
||||
Ollama-shaped /api/chat endpoint, e.g.
|
||||
"http://localhost:11434/api/chat".
|
||||
de: |
|
||||
Ollama-kompatibler /api/chat-Endpunkt, z. B.
|
||||
"http://localhost:11434/api/chat".
|
||||
model:
|
||||
type: text
|
||||
description:
|
||||
en: Model identifier as registered with the endpoint, e.g. "qwen2.5:14b" or "llama3.1:8b".
|
||||
de: Modell-Identifier wie beim Endpunkt registriert, z. B. "qwen2.5:14b" oder "llama3.1:8b".
|
||||
api_key:
|
||||
type: text
|
||||
description:
|
||||
en: |
|
||||
Optional bearer token for cloud-hosted endpoints.
|
||||
Empty string means no Authorization header is sent.
|
||||
de: |
|
||||
Optionaler Bearer-Token für Cloud-Endpunkte.
|
||||
Leer = kein Authorization-Header gesendet.
|
||||
system_prompt:
|
||||
type: text
|
||||
description:
|
||||
en: |
|
||||
Optional system prompt. Empty string omits the system
|
||||
message and lets the model use its built-in default.
|
||||
de: |
|
||||
Optionaler System-Prompt. Leer = das Modell verwendet
|
||||
seinen eingebauten Default.
|
||||
|
||||
# Outputs produced.
|
||||
outputs:
|
||||
# The assistant's plain-text reply.
|
||||
response: text
|
||||
# The endpoint the response was generated against. Used for
|
||||
# audit correlation.
|
||||
model_endpoint: text
|
||||
# The model identifier as supplied to the LLM API.
|
||||
model_name: text
|
||||
# SHA-256 digest of the served Ollama model, when reachable.
|
||||
# Empty for non-Ollama endpoints (OpenAI / Anthropic do not
|
||||
# expose a digest API). Together with model_endpoint and
|
||||
# model_name this answers the audit question "which exact
|
||||
# model produced this response?"
|
||||
model_digest: text
|
||||
response:
|
||||
type: text
|
||||
description:
|
||||
en: The assistant's plain-text reply.
|
||||
de: Die Klartext-Antwort des Modells.
|
||||
model_endpoint:
|
||||
type: text
|
||||
description:
|
||||
en: The endpoint the response was generated against. Used for audit correlation.
|
||||
de: Der Endpunkt, gegen den die Antwort erzeugt wurde — für Audit-Korrelation.
|
||||
model_name:
|
||||
type: text
|
||||
description:
|
||||
en: The model identifier as supplied to the LLM API.
|
||||
de: Der an die LLM-API übergebene Modell-Identifier.
|
||||
model_digest:
|
||||
type: text
|
||||
description:
|
||||
en: |
|
||||
SHA-256 digest of the served Ollama model, when reachable.
|
||||
Empty for non-Ollama endpoints (OpenAI / Anthropic do not
|
||||
expose a digest API). Together with model_endpoint and
|
||||
model_name this answers the audit question "which exact
|
||||
model produced this response?"
|
||||
de: |
|
||||
SHA-256-Digest des bedienten Ollama-Modells, wenn erreichbar.
|
||||
Leer für Nicht-Ollama-Endpunkte (OpenAI / Anthropic stellen
|
||||
keine Digest-API bereit). Zusammen mit model_endpoint und
|
||||
model_name beantwortet das die Audit-Frage "welches genaue
|
||||
Modell hat diese Antwort erzeugt?"
|
||||
|
||||
# Permissions required.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
mod llm;
|
||||
|
||||
use fai_module_sdk::prelude::*;
|
||||
use chain_module_sdk::prelude::*;
|
||||
|
||||
#[fai_module]
|
||||
pub fn invoke(_ctx: Context, inputs: Inputs) -> Result<Outputs, ModuleError> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue