AI examples
A tour of the AI capability — text generation, typed values instead of text to parse, streaming to a browser, tools the model calls, agents that run as jobs, embeddings, and how to test all of it without a provider.
Read AI first for the concepts — these files show them wired together.
git clone https://gitlab.finema.co/finema/idin-core.git
cd idin-core/v2/examples/ai
go run .Generating
The three shapes a generation comes in: text for a person to read, a Go value for code to use, and a stream for a user who is waiting.
Letting the model act
Tools turn a generation into something that can read and change data, which makes the ceiling and the approval gate part of the feature rather than an afterthought.
Retrieval and tests
Vectors for search, and the fixtures that let every example above be tested without an API key.
Putting it together
main.go wires one App and runs every example in this topic, so go run . works with no AI configuration at all — with no AI_PROVIDER set it substitutes the in-memory model and says so in the boot log, rather than failing or quietly answering nothing.
// Command ai is a runnable tour of the v2 AI capability: text generation,
// typed values, streaming, tool use, agents as jobs, embeddings and testing.
//
// Each example lives in its own file:
//
// 01_text.go Generate, conversations, FinishReason, retryable vs permanent
// 02_typed.go llm.New[T] — a Go value instead of a paragraph to parse
// 03_streaming.go Stream to a browser as SSE, and what to keep at the end
// 04_tools.go tools the model calls, MaxSteps, Approve, the audit trail
// 05_agent_job.go the long loop belongs in a job, not in a bare goroutine
// 06_embeddings.go vectors, similarity, a minimal RAG
// 07_testing.go the same code under test with no provider and no bill
//
// Run it with: go run ./examples/ai
//
// It runs with no AI configuration at all. Unconfigured, the framework hands
// out the disabled model, whose every call fails with LLM_DISABLED — loudly,
// because a generation that quietly returned nothing is an empty answer the
// caller believes is real. Right for a service, useless for a demo, so this
// program substitutes the in-memory model and says so in the boot log.
//
// With a provider configured every step below is a real, billed call.
package main
import (
"context"
"fmt"
"os"
"time"
core "gitlab.finema.co/finema/idin-core/v2"
"gitlab.finema.co/finema/idin-core/v2/llm/goai"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "ai example:", err)
os.Exit(1)
}
}
func run() error {
env, err := core.NewEnv()
if err != nil {
return err
}
// goai.New returns the *disabled* model rather than an error when nothing is
// configured, so a service that does not use AI still boots. A provider name
// that is set but misspelled is an error: booting past a typo turns a
// five-second fix into a runtime mystery.
model, err := goai.New(env)
if err != nil {
return err
}
embedder, err := goai.NewEmbedder(env)
if err != nil {
return err
}
usingStandIn := !model.Enabled()
if usingStandIn {
model = core.NewMemoryLLM()
}
if !embedder.Enabled() {
embedder = core.NewMemoryEmbedder(embedDimensions)
}
app, err := core.NewApp(env, core.WithLLM(model), core.WithEmbedder(embedder))
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
defer func() { _ = app.Shutdown(context.Background()) }()
c := app.NewContext(ctx, core.ModeTest)
c.Log().Info("ai example starting",
"provider", model.Provider(),
"model", model.Model(),
"embedder", embedder.Provider(),
"stand_in", usingStandIn)
// Tools are built once, at startup: describing an input type is reflection,
// and a type that cannot be described is a bug that should stop the process
// rather than the tenth request of the day.
desk, err := NewSupportDesk()
if err != nil {
return err
}
demoGeneration(c, usingStandIn)
demoTools(c, desk)
demoEmbeddings(c)
return demoAgentJob(app, desk)
}
// demoGeneration — 01_text.go and 02_typed.go. The memory model echoes the last
// user turn, so what the first half proves is the plumbing: the request was
// built, an answer came back with a finish reason, the usage was recorded.
//
// The second half deliberately shows the failure path when the stand-in is in
// use: the memory model answers "{}" to any schema, so the Validate rule
// rejects an invoice with no total rather than storing a zero as though it had
// been read.
func demoGeneration(c core.IContext, usingStandIn bool) {
if out, err := summarize(c, "ยอดขายเดือนกรกฎาคมโต 12% จากเดือนก่อน สินค้าหมวดอาหารโตมากที่สุด"); err != nil {
c.Log().Warn("summarise failed", "code", err.GetCode(), "err", err)
} else {
c.Log().Info("summarised", "text", out)
}
inv, err := extractInvoice(c, "ACME CO., LTD. INV-2026-0731 Total 1,250.50 THB Due 31/08/2026")
switch {
case err == nil:
c.Log().Info("extracted invoice", "vendor", inv.Vendor, "total", inv.Total)
case usingStandIn:
c.Log().Info("validation rejected the stand-in's empty answer, as it should", "code", err.GetCode())
default:
c.Log().Warn("extraction failed", "code", err.GetCode(), "err", err)
}
}
// demoTools — 04_tools.go. Given tools and no script, the memory model calls
// every one of them once and then answers — which is how a service that adds a
// tool gets its handler exercised without anyone writing a script for it. The
// refund is denied by the policy on the way through.
func demoTools(c core.IContext, desk *SupportDesk) {
answer, err := desk.Answer(c, "ของออเดอร์ TH-1042 อยู่ไหนแล้ว และขอคืนเงินด้วย")
if err != nil {
c.Log().Warn("agent failed", "code", err.GetCode(), "err", err)
return
}
c.Log().Info("agent answered", "text", answer)
}
// demoEmbeddings — 06_embeddings.go. The memory embedder hashes words into a
// vector: deterministic, so a ranking assertion never flakes, and meaningless,
// so it exercises the pipeline rather than the search quality.
func demoEmbeddings(c core.IContext) {
corpus, err := indexChunks(c, "handbook", []string{
"Parcels are dispatched within two working days.",
"Refunds are returned to the original card within five working days.",
"Our office is closed on public holidays.",
})
if err != nil {
c.Log().Warn("indexing failed", "code", err.GetCode(), "err", err)
return
}
answer, err := answerFromCorpus(c, "How long do refunds take?", corpus)
if err != nil {
c.Log().Warn("rag failed", "code", err.GetCode(), "err", err)
return
}
c.Log().Info("answered from the corpus", "passages", len(corpus), "text", answer)
}