Skip to main content
CogniCore Header

A Character Intelligence Framework for Believable AI NPCs

Memory, emotion, relationships, rumors, and secrets — for characters that evolve through interaction instead of running on scripts.

Python Version License: MIT PyPI version PRs Welcome

Quickstart · Use Cases · Silent Hollow Demo · LLM Providers · Architecture · Known Limitations


What is CogniCore?

Most "AI NPC" demos are a chat window wired to an LLM — send a prompt, get a reply, forget everything when the session ends.

CogniCore is the layer underneath that. It's a multi-agent character runtime: persistent memory backed by a local vector store, an emotion engine that decays over time instead of resetting, a relationship graph between every pair of characters, and a rumor/secret system where information mutates and spreads the way gossip actually does. You wire an LLM (or nothing at all — it runs fully offline) into it for the actual text generation; CogniCore handles everything around why a character says what it says.

Core philosophy: predictable rules, unpredictable stories

  • Predictable rules — emotions decay on a tick-based clock toward a baseline, memories are retrieved by similarity search, rumors mutate based on the speaker's honesty and suspicion, relationships shift by fixed weights per social event.
  • Unpredictable stories — because those rules interact across many characters and many ticks, no two simulation runs play out the same way. Nobody scripts who turns on whom; the simulation works it out.

✨ Features

System What it actually does
🧠 Multi-tiered memory Short-term (sliding window), long-term, episodic, social, and secret memory, all queryable by similarity search through a local SQLite store — no external vector DB required
🔍 Built-in RAG Pure-Python TF-IDF cosine similarity by default (zero dependencies). Automatically upgrades to real OpenAI/Gemini embeddings if you configure an API key
🎭 Emotion engine 7 tracked dimensions (anger, fear, suspicion, happiness, trust, guilt, confidence), each decaying toward a baseline at its own configurable rate
🕸️ Relationship graph Per-pair, directional metrics (trust, respect, fear, friendship, rivalry, loyalty), exportable as JSON for D3/Vis.js-style visualization
🎯 Goals & planning Pick between a zero-dependency rule-based planner or LLM-assisted planning that reasons over personality, memory, and world state
🤫 Rumors & secrets Rumors mutate via rule-based text transformation as they pass between characters with different honesty/suspicion levels; secrets are shared or exposed based on trust thresholds, not coin flips
🔌 Pluggable LLMs OpenAI, Gemini, Claude, Ollama, LoomGPT, or a deterministic mock mode that needs no API key at all
🎮 Showcase demo Silent Hollow — a playable murder-mystery sim built on top of the SDK, with a FastAPI backend and a Three.js isometric frontend

📦 Installation

pip install cognicore-sdk

Note: the published 0.1.0 package requires pydantic. Please ensure you run pip install pydantic if installing the raw wheel, or install from source (see below).

To install from source instead:

git clone https://github.com/Karthik-Unni/CogniCore.git
cd CogniCore
pip install -r requirements.txt
pip install -e .

CogniCore runs completely offline by default — the default LLMClient provider is "mock", which uses rule-based planning and TF-IDF similarity search with no API key and no network calls. To use a real LLM, set a provider when constructing your LLMClient:

pip install cognicore-sdk[openai]      # OpenAI models
pip install cognicore-sdk[gemini]      # Google Gemini
pip install cognicore-sdk[anthropic]   # Claude
# Ollama and LoomGPT need no extra packages — they communicate over plain local HTTP

🚀 Quickstart

This example is copied directly from a working run against the current codebase — every line of output below is real, not illustrative.

from cognicore import Character, World, SimulationOrchestrator, Goal, SQLiteVectorStore

# A local, file-backed vector store — no external DB needed
vector_store = SQLiteVectorStore(db_path="my_world.db")

world = World()
orchestrator = SimulationOrchestrator(world)

maya = Character(
    agent_id="maya",
    name="Maya",
    personality={"honesty": 0.3, "curiosity": 0.8},
    goals=[Goal(id="idle", description="Go about daily business")],
    vector_store=vector_store,
)
orchestrator.register_character(maya)

# Characters react emotionally to events with trigger keywords
# (accused, threatened, caught_lying, secret_shared, etc.) out of the box
maya.emotions.process_event("accused", severity=1.0)
print(maya.emotions.get_state())
# {'anger': 0.25, 'fear': 0.2, 'suspicion': 0.25, 'happiness': 0.1, 'trust': 0.5, 'guilt': 0.0, 'confidence': 0.5}

# Run a tick of the simulation — Maya evaluates goals, retrieves
# relevant memories, and decides her next action
orchestrator.step()
print(maya.last_action)
# {'type': 'MOVE', 'target': 'Tavern', 'metadata': {'reason': 'Look for customers'}}

Notes on what's actually happening here, since the API is intentionally low-magic:

  • Character is the core agent class — there's no separate Agent wrapper or YAML config loader. Personality is currently a plain Dict[str, float] you define yourself (e.g. {"honesty": 0.3, "greed": 0.7}); the specific keys you use are then read by your own goal/planning logic.
  • emotions.process_event(...) applies a predefined delta set for common events (accused, threatened, gift_received, caught_lying, secret_shared, evidence_found). Character.observe(description) is the free-text path, but it currently only updates emotions when the text contains specific trigger keywords (murder/dead/body, stole/theft, threat/argue) — it isn't doing general sentiment analysis on arbitrary text yet.
  • orchestrator.step() runs one full tick: broadcasting local observations, letting every registered character plan and act, resolving those actions against the World, then decaying emotions and consolidating short-term memory into long-term summaries.

Persistent memory + retrieval

maya.memory.add_secret(
    "I stole gold from the church",
    secrecy_level=0.8, expose_penalty=0.6, timestamp=0, related_entities=[]
)

results = maya.memory.query_memories("gold", k=3)
print([m.content for m in results])
# ['I stole gold from the church']

This is backed by SQLiteVectorStore, which uses hand-rolled TF-IDF cosine similarity over your stored memories by default — no embedding API call required. If you attach an LLMClient configured for OpenAI or Gemini, it transparently switches to real embedding-based similarity instead.

Rumors that mutate as they spread

from cognicore import Rumor

rumor = Rumor(
    id="r1", origin_agent_id="elena", about_agent_id="alden",
    content="Alden was seen at late night near the vault.", timestamp=0.0,
)

# A dishonest, suspicious speaker distorts the story on retelling
mutated = rumor.mutate(speaker_honesty=0.1, speaker_suspicion=0.7)
print(mutated.content)
# 'Alden was seen acting suspiciously sneaking around at midnight near the vault.'
print(round(mutated.fidelity, 2))
# 0.84

Whether a listener believes a rumor depends on how much they trust the source — RumorTracker.hear_rumor() rejects rumors from low-trust speakers and accepts high-fidelity rumors from trusted ones at different probabilities, rather than a fixed coin flip.


🎮 Use Cases

1. A companion character that remembers and reacts

from cognicore import Character, SQLiteVectorStore, Goal

companion = Character(
    agent_id="jin", name="Jin",
    personality={"warmth": 0.7, "patience": 0.4},
    goals=[Goal(id="idle", description="Be a good friend")],
    vector_store=SQLiteVectorStore(db_path="jin.db"),
)

companion.observe("I threatened to leave if things don't change.", timestamp=0)
print(companion.emotions.get_state())

Every observe() call writes to persistent memory and nudges the emotion state — and because the vector store is a local SQLite file, the character's memory survives across process restarts.

2. A small social sim where gossip spreads and mutates

Register a handful of Characters with a shared SQLiteVectorStore, seed one of them with a Rumor, and call orchestrator._resolve_conversation(speaker, listener) (or just run orchestrator.step() repeatedly with characters placed in the same World location) to watch the story warp as it passes between agents with different honesty levels.

3. Drive characters with an LLM instead of rules

from cognicore import Character, SQLiteVectorStore, Goal
from cognicore.llm.client import LLMClient

llm = LLMClient({"provider": "openai", "api_key": "sk-...", "model": "gpt-4o-mini"})

wizard = Character(
    agent_id="gandry", name="Gandry",
    personality={"openness": 0.9},
    goals=[Goal(id="seek_knowledge", description="Uncover the ruins' secret")],
    vector_store=SQLiteVectorStore(db_path="gandry.db", llm_client=llm),
    llm_client=llm,
)

Pass any LLMClient with a non-mock provider into Character, and the planner automatically switches from rule-based logic to LLM-assisted reasoning over personality, emotions, relationships, and retrieved memories.


🏘️ Silent Hollow — Showcase Demo

silent_hollow_demo/ is a playable murder-mystery built on top of the SDK — 8 named characters with secrets, emotions, and relationships, a FastAPI backend running the simulation loop, and a Three.js isometric frontend for watching it unfold.

# Backend
cd silent_hollow_demo/backend
pip install fastapi uvicorn
uvicorn server:app --reload

# Frontend
cd silent_hollow_demo/frontend
npm install
npm run dev

Open the frontend dev server URL and watch the village — characters investigating, gossiping, forming alliances, and occasionally giving themselves away.

Worth knowing: the demo's locations (Tavern, Blacksmith, Town Hall...) and goal types (solve_murder, hide_crime, self_preservation) are currently hardcoded directly into the SDK's core World and Goal classes, not cleanly separated into demo-only config. If you're building your own world rather than running Silent Hollow, you'll currently need to either work around or fork these classes — see Known Limitations.


🔌 LLM Providers

LLMClient is configured with a plain dict, not a separate adapter class per provider:

from cognicore.llm.client import LLMClient

# No API key needed — deterministic rule-based responses
mock = LLMClient({"provider": "mock"})

# Real providers
openai_client = LLMClient({"provider": "openai", "api_key": "sk-...", "model": "gpt-4o-mini"})
gemini_client = LLMClient({"provider": "gemini", "api_key": "...", "model": "gemini-1.5-flash"})
claude_client = LLMClient({"provider": "claude", "api_key": "...", "model": "claude-3-5-sonnet-20240620"})
ollama_client = LLMClient({"provider": "ollama", "api_url": "http://localhost:11434", "model": "llama3"})
loomgpt_client = LLMClient({"provider": "loomgpt", "api_url": "http://localhost:8080/v1", "model": "loomgpt-local"})

If an API call fails for any reason, LLMClient.generate() automatically falls back to mock output rather than raising — useful for keeping a long-running simulation alive through transient API errors, though it does mean failures can silently degrade output quality rather than surfacing loudly.

Training your own local model

If you want characters that run on a custom-trained model instead of a hosted API, LOOM-GPT (pip install loom-gpt) is a companion project — a from-scratch, hackable PyTorch implementation for training small, domain-specific transformers locally and inspecting how they generate. It's aimed at understanding and experimenting with GPT-style models, not at replacing a hosted LLM's general capability — pair it with CogniCore by serving its output through a small local HTTP wrapper and pointing an LLMClient at it with provider: "loomgpt"-style config, or by utilizing LoomGPT's standard OpenAI-compatible completions API.


🧩 Architecture

cognicore/               # The SDK
├── agents/             # Character class — the core agent runtime loop
├── emotions/           # EmotionEngine: tick-based decay, event-driven updates
├── goals/              # Goal class with dynamic priority evaluation
├── llm/                # LLMClient — single class, multi-provider via config dict
├── memory/             # MemoryManager + Memory/MemoryType models
├── planning/            # Planner — rule-based fallback or LLM-assisted
├── rag/                 # SQLiteVectorStore — TF-IDF or real embeddings
├── relationships/       # RelationshipManager — per-pair directional metrics
├── rumors/               # Rumor model + RumorTracker (mutation, belief)
├── secrets/              # SecretSystem — static methods for share/expose logic
├── social_graph/         # Exports relationship data as graph JSON
├── world/                # World — locations, agent placement, clue tracking
└── simulation/         # SimulationOrchestrator — the multi-agent tick loop

silent_hollow_demo/       # Showcase app (NOT required to use the SDK)
├── backend/             # FastAPI server wrapping SimulationOrchestrator
└── frontend/            # Vite + React + Three.js dashboard

tests/                   # 6 unit/integration tests, all passing as of this writing

⚠️ Known Limitations & Roadmap

Being upfront about where this stands today:

  • Packaging bug: the published cognicore-sdk==0.1.0 on PyPI requires pydantic. Please ensure you run pip install pydantic if installing the raw wheel, or install from source (where this is already fixed).
  • Demo logic lives inside the SDK core. World hardcodes Silent Hollow's locations and murder-mystery state (murder_discovered, killer_id); Goal.evaluate_priority() hardcodes goal IDs like solve_murder and hide_crime; the rule-based Planner fallback only knows how to plan for those same goal IDs and locations. The LLM-assisted planning path is properly generic — this only affects the zero-API-key path. Next step: extract Silent Hollow's specifics into a silent_hollow_demo-local subclass or config, leaving World/Goal/Planner truly scenario-agnostic.
  • No YAML/config-based character definition yet — characters are constructed directly in Python. A Character.from_config() loader is a natural next addition if you want non-developers authoring characters.
  • observe()'s emotional impact is keyword-triggered, not general sentiment analysis — it currently recognizes a small fixed set of trigger words. Fine for the demo's murder-mystery vocabulary, narrow for general-purpose use.
  • No license file is currently committed to the repository despite the badge above — add a LICENSE file with the actual MIT text before relying on that badge being meaningful to users or contributors.

🤝 Contributing

Issues and PRs welcome — especially around decoupling the demo-specific logic from the core SDK classes noted above, or adding new LLM provider adapters.


📄 License

MIT — see LICENSE. (Add the LICENSE file to the repo root for this to be enforceable.)

Metadata

Release files for cognicore-sdk 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cognicore-sdk 0.1.1
File Size Uploaded
cognicore_sdk-0.1.1.tar.gz 42.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cognicore-sdk 0.1.1
File Interpreter ABI Platform
cognicore_sdk-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 82.3 kB

Release files / cognicore_sdk-0.1.1.tar.gz

Download URL cognicore_sdk-0.1.1.tar.gz
Size 42.2 kB
Tags Source
SHA-256 checksum
How to use checksums
fcbd747e6229d728248ee57a4b3a7b4b785724b7c18a33ae46f407fc5fd1317d
BLAKE2b-256 checksum
How to use checksums
40542dc5ddc3f801675b12d10c0fd8c8f2114b9a5510b9f24a3d5f3cd463a305
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.0

Release files / cognicore_sdk-0.1.1-py3-none-any.whl

Download URL cognicore_sdk-0.1.1-py3-none-any.whl
Size 40.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4c8c708ce1dc46638d944304f4b5319c2906d6202e7fabd1a9813ca8a1e7322b
BLAKE2b-256 checksum
How to use checksums
cf72788687f4887d370fb5c5d63826795a91063f6c8bcb0e1fa08872772da636
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.0

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page