> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deyta.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Workloads

> End-to-end scenarios that compose Khora's APIs into real applications: a support-ticket knowledge graph, namespace versioning, cross-document entity resolution, and multimodal document QA.

The [`examples/30_workloads/`](https://github.com/DeytaHQ/khora/tree/main/examples/30_workloads)
tier is where the [Basics](/khora/examples/basics) and
[Core APIs](/khora/examples/core-apis) come together into applications you'd
actually ship. Each is a single runnable file with a heavily commented walkthrough.
The summaries below distil what each one teaches and the one call that carries it.

<Note>
  Most of these workloads lean on VectorCypher's knowledge graph, so they're best on
  PostgreSQL + Neo4j. Run `make dev` in the khora repo to bring them up, and pass
  `--config examples/khora.standard.yaml` to point at that stack. They also run on the
  zero-infra embedded backend (`pip install "khora[sqlite-lance]"` + `OPENAI_API_KEY`),
  where entity-vector search is brute-force (no ANN index), so write-time dedup and some
  graph reads are slower.
</Note>

| Workload               | Teaches                                                                     |
| ---------------------- | --------------------------------------------------------------------------- |
| Support-ticket graph   | Vector search vs. multi-hop graph traversal                                 |
| Namespace versioning   | The dual-UUID model and the storage API                                     |
| Resume search          | Full extraction + cross-document entity resolution                          |
| Multimodal document QA | Vision-model figure descriptions + cited RAG over a mixed text/image corpus |

## Support-ticket knowledge graph

Ingest \~100 support tickets, then ask two shapes of question. *"Tickets about login
failures"* is a ranked-list problem. Vector search nails it. *"Give me context
around Acme Logistics"* is a **neighborhood** problem. The answer is Acme's tickets,
the products Acme uses, the agents handling them, the error categories, and other
customers on the same products. That's a 2-hop graph walk, not a similarity search.

```python theme={null}
batch = await kb.remember_batch(docs, namespace=ns_id,
                                entity_types=["ORGANIZATION", "PRODUCT", "CONCEPT", "PERSON"],
                                relationship_types=["AFFECTS", "ASSIGNED_TO", "RELATES_TO", "MENTIONS"])

# vector shape:
await kb.recall("tickets about login failures", namespace=ns_id, limit=5)

# graph shape — walk outward from the customer entity:
acme = _find_entity(await kb.list_entities(namespace=ns_id, entity_type="ORGANIZATION"), "Acme")
related = await kb.find_related_entities(acme.id, namespace=ns_id, max_depth=2, limit=25)
```

**Takeaway:** pick the retrieval shape to match the question. The entity taxonomy
is chosen deliberately (customer = `ORGANIZATION`, SKU = `PRODUCT`, error category =
`CONCEPT`, agent = `PERSON`) so the multi-hop walk has real structure to follow.

## Namespace versioning

A `MemoryNamespace` carries two UUIDs: `namespace_id` is the **stable** id (same
across every version: hold this in your application code), and `id` is the
**row primary key**, distinct per version. The high-level facade
(`kb.remember` / `kb.recall`) takes the stable id and resolves to the active
version. The storage layer (`kb.storage.*`) takes a row id to address a *specific*
version.

```python theme={null}
v1 = await kb.create_namespace()
stable_id, v1_row = v1.namespace_id, v1.id

await kb.remember(text, namespace=stable_id, ...)            # facade → active version

v2 = await kb.storage.create_namespace_version(previous_version=v1)  # atomically flips active
active_row = await kb.storage.resolve_namespace(stable_id)   # stable id → active row id
v1_entities = await kb.storage.list_entities(v1_row)         # read a frozen older version
```

**Takeaway:** one version is active at a time. Historical versions are read-only and
addressable only through the storage layer by row id. The pattern earns its keep for
re-indexing with a better extractor, A/B-testing a chunking change, or snapshotting a
graph before re-ingesting.

## Resume search

The entity-centric workload where dedup *is* the story: 50 résumés where "Stripe",
"Stripe Inc.", "Stripe, Inc." and "stripe.com" must resolve to one node, and "k8s"
/ "K8s" / "Kubernetes" to another, before you can answer *"Stripe alumni who know
Kubernetes."* The work lives in the `ExpertiseConfig`: a system prompt that
canonicalizes surface forms at extraction time, plus typed entities and
relationships whose `identifiers` drive cross-document matching.

```python theme={null}
from khora import ExpertiseConfig, EntityTypeConfig, RelationshipTypeConfig
from khora.engines.vectorcypher.engine import VectorCypherConfig

expertise = ExpertiseConfig(
    name="recruiter_demo",
    description="Recruiter-style ontology: candidates, employers, skills.",
    # The system prompt does the canonicalization that dedup alone can't,
    # which matters most on the embedded backend (no entity-vector index).
    system_prompt=(
        "Extract from short candidate résumés. Rules:\n"
        "1. Companies: canonicalize to the brand, no legal suffix or URL "
        "('Stripe Inc.', 'Stripe, Inc.', 'stripe.com' all become 'Stripe').\n"
        "2. Skills: canonicalize aliases ('k8s'/'K8s' to 'Kubernetes', "
        "'Postgres' to 'PostgreSQL', 'Golang' to 'Go').\n"
        "3. Negation: no HAS_SKILL edge for a skill the candidate explicitly lacks.\n"
        "4. One CANDIDATE per person, by full name."
    ),
    entity_types=[
        EntityTypeConfig(name="CANDIDATE", description="A job candidate, by name.",
                         identifiers=["name"]),
        EntityTypeConfig(name="COMPANY", description="A current or past employer.",
                         identifiers=["name"], aliases=["EMPLOYER", "ORGANIZATION"]),
        EntityTypeConfig(name="SKILL", description="A technical skill or technology.",
                         identifiers=["name"], aliases=["TECHNOLOGY", "TOOL"]),
    ],
    relationship_types=[
        RelationshipTypeConfig(name="WORKED_AT", description="CANDIDATE employed by COMPANY.",
                               source_types=["CANDIDATE"], target_types=["COMPANY"],
                               properties=["start_year", "end_year", "role"]),
        RelationshipTypeConfig(name="HAS_SKILL", description="CANDIDATE has experience with SKILL.",
                               source_types=["CANDIDATE"], target_types=["SKILL"]),
    ],
)

# Full extraction (every chunk; résumés have no filler) + the ontology.
engine_kwargs = {"vectorcypher_config": VectorCypherConfig(skeleton_core_ratio=1.0,
                                                           min_extraction_tokens=0)}
batch = await kb.remember_batch(
    resumes, namespace=ns_id, expertise=expertise,
    entity_types=expertise.get_entity_type_names(),
    relationship_types=expertise.get_relationship_type_names(),
)
# Cross-document unification (the expansion phase) then collapses surface-form
# variants into one node per entity, keyed off each type's `identifiers`.
# A two-set graph intersection answers "Stripe alumni ∩ Kubernetes users".
```

**Takeaway:** the `ExpertiseConfig` is the whole game. `entity_types` /
`relationship_types` give the graph its shape, `identifiers` tell the unifier what
"the same entity" means, and the `system_prompt` canonicalizes variants the matcher
would otherwise miss. See [Expertise & ontologies](/khora/pipeline/expertise) for
the full surface (attributes, inference rules, confidence). Defaults to PostgreSQL +
Neo4j. The embedded backend has no entity-vector index, so similarity dedup can't
catch variants the LLM didn't already collapse.

## Multimodal document QA

khora indexes text, so "multimodal" is a pipeline shape, not a separate API: turn each
non-text element into text, then ingest everything uniformly. This workload takes a corpus
of Markdown docs whose figures are embedded as image links (`![alt](path)`). Each figure is
described by a vision model (its alt text steers the description), so the picture becomes
searchable text tagged with its image path. Every chunk gets a stable `external_id`, so a
retrieval-augmented answer can cite exactly which sections and figures it drew on.

```python theme={null}
# 1. Describe each embedded figure with a vision model (app-level; alt text steers it),
#    then ingest text sections and figure descriptions in one batch. Each chunk keeps a
#    stable external_id, and the image path rides in metadata.
figure_desc = await describe_image(fig_path, alt_text, client=vision_client)
batch = [
    {"content": section_text, "external_id": f"{doc}#{heading}",
     "metadata": {"kind": "text"}},
    {"content": f"figure: {figure_desc}", "external_id": f"{doc}#figure-{fig}",
     "metadata": {"kind": "image", "image_path": str(fig_path)}},
]
await kb.remember_batch(
    batch, namespace=ns_id,
    entity_types=["SPACECRAFT", "INSTRUMENT", "LOCATION", "MEASUREMENT", "MISSION"],
    relationship_types=["RELATES_TO", "PART_OF", "LOCATED_IN"],
)

# 2. RAG: recall the top chunks, answer from that context only, cite each source.
result = await kb.recall(question, namespace=ns_id, limit=10)
sources = [d.external_id for d in result.documents]   # what the answer is grounded in
```

**Takeaway:** khora is text-native, so the figure-describe step is app-level (a vision model
you call, not a khora API). Once figures are text, VectorCypher extracts entities
(spacecraft, instruments, locations) from both the prose and the figure descriptions, so a
cross-document question can reach a fact that lives only in a diagram. The stable
`external_id` per chunk is what makes the answer citable. This one needs a vision-capable
model on top of the usual `OPENAI_API_KEY`.

## Next steps

<CardGroup cols={2}>
  <Card title="Integrations" icon={<span className="material-symbols-sharp">extension</span>} href="/khora/integrations/overview">
    Wire these patterns into CrewAI, LangGraph, Google ADK, OpenAI Agents, or
    LlamaIndex.
  </Card>

  <Card title="VectorCypher" icon={<span className="material-symbols-sharp">settings_input_component</span>} href="/khora/engines/vectorcypher">
    How the engine fuses vector, graph, and keyword search behind these examples.
  </Card>
</CardGroup>
