Persian-first resilience assistant with GraphML-grounded retrieval, local LLM inference, structured assessment, and persistent user context.
ResiGraph is a research-oriented resilience-domain assistant that combines a checked-in Knowledge Graph, bounded graph retrieval, local language-model inference through Ollama, interactive conversational flows, resilience assessment, and lightweight user/profile persistence. The codebase is built to study how structured domain knowledge and user context can ground generation without relying on a remote model service.
ResiGraph is designed for resilience-related dialogue and assessment. It takes a user message, extracts resilience signals, maps them into a canonical resilience vocabulary, queries a GraphML-backed Knowledge Graph, constructs grounded context, and then uses a local LLM to produce a Persian response. When the evidence is incomplete or ambiguous, the system can ask a bounded clarification question. The same assessment output can then feed a counseling layer that produces personalized, graph-backed support guidance.
The central engineering goal is not to replace model reasoning with a graph, but to keep generation tethered to explicit domain artifacts:
- the Knowledge Graph stores curated resilience concepts and relations
- retrieval selects only bounded, relevant graph evidence
- assessment separates score, confidence, and coverage
- user/profile state is persisted locally in SQLite
- responses are generated by a local model provider rather than a hosted API
This makes the system suitable for prototyping, offline-oriented development, and reproducible research experiments.
ResiGraph exists because a general-purpose LLM is often not enough for a narrow domain such as resilience support. The implementation here explores several practical constraints and design goals:
- Domain grounding: resilience concepts are mapped to a canonical vocabulary rather than inferred freely.
- Traceability: graph edges and node evidence retain document and chunk provenance.
- Controlled retrieval: only bounded graph neighborhoods and evidence snippets are injected into prompts.
- Local inference: the default runtime uses Ollama, which keeps the system self-hostable.
- Personalization: stored user profile/state and prior clarification sessions can influence later turns.
- Assessment: the system produces explicit per-dimension scores, confidence, and coverage instead of a single opaque score.
The implementation does not claim to eliminate hallucinations or provide clinical validation. Instead, it explores whether structured grounding and explicit uncertainty handling can make responses more stable and inspectable.
- GraphML-backed resilience Knowledge Graph loaded at startup
- In-memory graph query engine with concept, relation, neighbor, and evidence lookups
- Graph-aware prompt grounding for conversational responses
- Unified chat orchestration that combines extraction, assessment, clarification, and counseling
- Persian-first RTL frontend
- Markdown rendering in chat messages
- Local theme toggle with persisted user preference
- Persistent anonymous user identity via HTTP cookie
- SQLite-backed users, conversations, messages, assessments, user state, and clarification sessions
- Interactive assessment flow with bounded clarification rounds
- Counseling generation from assessment outputs and graph evidence
- Evaluation framework with a 100-sample Persian benchmark
- Docker Compose workflow for backend and frontend containers
- Offline-oriented local model integration through Ollama, with optional vLLM support in code
flowchart LR
U[User]
B[Browser]
FE[Persian RTL Frontend]
API[FastAPI Backend]
CHAT[Conversation Orchestrator]
EX[Resilience Signal Extraction]
AS[Assessment Engine]
CL[Clarification / Question Planner]
CO[Counseling Service]
CM[Conversation Manager]
RET[Graph Query / Grounding]
KG[(GraphML Knowledge Graph)]
DB[(SQLite Persistence)]
LLM[Local LLM Provider]
U --> B --> FE --> API
API --> CHAT
CHAT --> EX
CHAT --> AS
CHAT --> CL
CHAT --> CO
CHAT --> CM
CM --> RET --> KG
AS --> RET
CO --> RET
CHAT --> DB
API --> DB
CM --> LLM
CHAT --> LLM
LLM --> API --> FE
The key boundary is between the browser/frontend and the backend. The backend owns graph loading, retrieval, extraction, assessment, clarification logic, conversation persistence, and model invocation.
sequenceDiagram
participant U as User
participant F as Frontend
participant A as Backend API
participant O as Conversation Orchestrator
participant E as Signal Extraction
participant S as Assessment Engine
participant P as Clarification Planner
participant C as Conversation Manager
participant G as Graph Query Engine
participant K as GraphML KG
participant M as Local LLM
participant D as SQLite
U->>F: Enter Persian message
F->>A: POST /api/chat
A->>O: chat(user_id, message, conversation_id)
O->>D: Load prior conversation/session state
O->>E: Extract resilience signals
O->>S: Assess mapped signals
S->>G: Query graph concepts / relations / evidence
G->>K: Resolve nodes, edges, evidence
K-->>G: Grounded graph data
G-->>S: Bounded KG context
O->>P: Decide whether clarification is needed
alt clarification needed
O-->>A: Clarification question + assessment
else normal or counseling-ready turn
O->>C: Generate natural reply with KG grounding
C->>M: Send prompt + history + system context
M-->>C: Completion
C-->>O: Assistant reply
end
O->>D: Persist conversation / session / messages
A-->>F: Chat response + state + optional counseling
F-->>U: Render Markdown response
The checked-in Knowledge Graph is stored in:
knowledge_graph/graphml/semantic_graph.graphml
The loader reads this GraphML file at application startup and stores it in memory. The graph is not built dynamically at runtime in this repository.
The graph loader expects GraphML with required node and edge attributes. Current runtime enforcement includes:
- node attributes:
type,aliases,document_ids,chunk_ids - edge attributes:
relation,head,tail,support_count,source_triples,source_records,document_ids,chunk_ids,evidence_refs
The repository-level checks confirm that the checked-in graph currently contains:
- 611 nodes
- 436 edges
Nodes are loaded into GraphNode objects with:
idnametypealiasesdocument_idschunk_idslabelsproperties
Edges are loaded into GraphEdge objects with:
idsource_idtarget_idrelationsupport_countsource_triplessource_recordsdocument_idschunk_idsevidence_refsheadtailproperties
Evidence is preserved as structured metadata rather than being collapsed into a single free-text string.
The runtime query engine is in-memory and supports:
get_concept(name)get_relation(relation, source=None, target=None)get_evidence_for_concept(name)get_neighbors(name)- node iteration for prompt grounding
Concept lookup works by node ID, canonical name, or alias. Relation lookup is exact on relation label with optional source/target filtering.
ResiGraph distinguishes between:
- source knowledge: the original graph artifact and its provenance metadata
- extracted claims: signals extracted from user text
- canonicalized knowledge: mapped resilience concepts and controlled relation vocabulary
- runtime retrieval: bounded concept, relation, neighbor, and evidence selection
- generated responses: model output conditioned on grounded context
This separation is important because it prevents runtime prompting from becoming indistinguishable from source truth.
The actual graph includes many domain concepts and relations. A simplified conceptual example is:
graph LR
RS[Resilience]
SS[Social Support]
C[Coping]
P[Protective Resources]
R[Risk / Stressor]
SS -->|supports| RS
C -->|supports| RS
P -->|strengthens| RS
R -->|related_to| RS
The exact edge labels depend on the checked-in GraphML artifact. The assessment engine currently treats a controlled subset of relations as supportive evidence, including supports, buffers, strengthens, enables, related_to, associated_with, and facilitating psychological resilience.
Knowledge in ResiGraph is not generated at runtime from scratch. The runtime consumes a pre-generated GraphML artifact and performs bounded retrieval over it.
flowchart LR
KGML[Checked-in GraphML]
LD[Loader]
ST[In-memory Storage]
QT[Query Engine]
GR[Grounding Helpers]
CT[Prompt Context]
LM[LLM Request]
KGML --> LD --> ST --> QT --> GR --> CT --> LM
The grounding layer only injects a small, bounded subset of concepts, neighbor names, relation traces, and evidence snippets into the model prompt.
Retrieval is intentionally conservative:
- the message is tokenized and matched against node IDs, names, and aliases
- only short, meaningful terms are considered
- matched concepts are bounded to a small number of concepts per turn
- each concept bundle includes a bounded set of neighbors, relations, and evidence items
- graph evidence is inserted as a hidden system context for the local LLM
For chat generation, retrieval is used only when the conversation manager is supplied with a graph query engine. The current orchestrator does pass the shared graph engine into the conversation manager.
For assessment, the graph is used more directly:
- validated resilience signals are mapped to canonical KG concepts
- direct concept evidence and supported outgoing relations are collected
- unsupported or unmapped signals remain visible as uncertainty
The assessment subsystem is implemented in backend/assessment/.
It produces a structured result with:
- signal mappings
- per-dimension assessments
- overall score
- confidence
- coverage
- risks
- an explanation block
- completeness status
These are separate values:
- score: the estimated resilience value for a dimension or overall summary
- confidence: how strongly the system believes the score is supported
- coverage: how much of the expected evidence space is actually populated
Missing information does not automatically imply a zero score. In the code, dimensions without sufficient support can remain null rather than being forced to zero.
flowchart TD
MSG[User message or clarification answer]
EXT[Signal extraction]
VAL[Validation / normalization]
MAP[KG signal mapping]
REA[Bounded graph reasoning]
SCO[Dimension scoring]
GAP[Clarification policy]
OUT[Assessment response]
CNL[Counseling preparation]
MSG --> EXT --> VAL --> MAP --> REA --> SCO --> GAP --> OUT --> CNL
The current implementation:
- extracts resilience signals from user text
- normalizes and sorts signals deterministically
- maps signal types or aliases into canonical KG concepts
- reasons only over explicitly supported graph relations and evidence
- scores predefined resilience dimensions
- computes an overall assessment from the dimension outputs
- tracks completeness when the available evidence is insufficient
The default configuration includes:
- social support
- coping capacity
- self-efficacy
- adaptability
- problem solving
- hope/optimism
- stress/risk exposure
- protective resources
The frontend assessment view is implemented in frontend/src/features/assessment/components/AssessmentView.tsx.
It displays:
- overall score
- confidence
- coverage
- a radar-style dimension visualization
- per-dimension cards
- strengths
- likely support needs
- insufficient-data states
Charts are only shown when the current assessment has enough meaningful data. If the evidence is thin, the UI falls back to a more cautious presentation instead of forcing a chart.
The assessment page also triggers the counseling service when the assessment is complete, so the UI can display graph-backed guidance alongside the assessment output.
ResiGraph uses several layers of user context:
- anonymous identity cookie:
resigraph_user_id - user profile stored in SQLite
- user state storage in SQLite
- per-conversation history in SQLite
- clarification session state in SQLite
- browser-local session history in
localStorage
The current implementation stores:
- user language
- user preferences
- metadata
- conversation messages
- conversation metadata and timestamps
- user state metadata and summaries
- clarification sessions with merged signals and assessment snapshots
Personalization is currently lightweight and mostly structural:
- conversation history is sent to the LLM as prior chat context
- the orchestrator merges prior clarification signals with new evidence
- clarification sessions persist state across follow-up rounds
- profile and state endpoints let the frontend display stored user context
flowchart LR
U[Anonymous user cookie]
P[User profile]
S[User state]
C[Conversation]
Q[Clarification session]
R[Chat / assessment response]
U --> P
U --> S
U --> C
C --> Q
Q --> R
R --> C
There is no destructive reset API in the backend. The frontend conversation reset clears the browser-side conversation state and the current conversation ID, but it does not delete rows from SQLite.
The frontend is a Vite + React + TypeScript application. It is Persian-first and RTL-aware.
- full-screen application shell
- right-to-left layout and Persian UI copy
- local theme toggle with persistence in
localStorage - chat view with independent scroll area
- auto-scroll and scroll-to-bottom behavior
- fixed composer at the bottom
- Markdown rendering for assistant messages
- message grouping for repeated speaker turns
- conversation reset confirmation
- assessment dashboard
- history view
- profile view
frontend/
├── src/
│ ├── App.tsx
│ ├── main.tsx
│ ├── styles/
│ ├── lib/
│ └── features/
│ ├── app/
│ ├── chat/
│ ├── assessment/
│ ├── counseling/
│ ├── history/
│ └── profile/
└── package.json
The chat view stores conversation history in browser localStorage and keeps the latest conversation ID separately so the UI can continue a conversation after refresh.
The backend is a FastAPI application that initializes SQLite and loads the Knowledge Graph once per process.
backend/main.py: FastAPI app factory and lifespan setupbackend/config.py: app and CORS settingsbackend/database/session.py: SQLite schema creation and connection helpersbackend/graph/: GraphML loading, storage, query, and grounding helpersbackend/llm/: provider abstraction plus Ollama and vLLM adaptersbackend/resilience/: extraction models and validation utilitiesbackend/assessment/: mapping, reasoning, scoring, and response buildingbackend/clarification/: clarification policy, planning, and session persistencebackend/conversation/: conversation persistencebackend/users/: profile persistencebackend/memory/: user state storagebackend/counseling/: counseling generationbackend/api/routes/: HTTP endpoints
- the API resolves or creates an anonymous user ID
- the orchestrator persists the incoming message
- resilience signals are extracted and validated
- signals are merged with prior clarification evidence
- assessment computes mappings, dimensions, and overall results
- clarification policy decides whether more evidence is needed
- conversation manager adds graph-grounded prompt context when available
- the local LLM generates the final natural-language reply
- conversation/session state is persisted
Only the routes that actually exist in the repository are listed here.
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/health |
Backend health check |
POST |
/api/chat |
Submit a chat turn and receive a unified response |
GET |
/api/chat/history |
Retrieve stored conversation history for the current anonymous user |
POST |
/api/assessment/assess |
Score validated resilience signals against the graph |
POST |
/api/assessment/interactive |
Run the clarification-enabled assessment flow |
GET |
/api/kg/concept/{name} |
Fetch a concept, its relations, and evidence |
GET |
/api/kg/relation |
Query edges by relation, source, and/or target |
POST |
/api/counseling/personalized |
Generate counseling output from an assessment payload |
GET |
/api/counseling/health |
Health check for the counseling route |
POST |
/api/users/anonymous |
Create or return the anonymous user identity cookie |
GET |
/api/users/me |
Return the current user profile |
GET |
/api/users/me/state |
Return the current user state record |
- chat requests accept
message, optionalconversation_id, optionalhistory, and optionalmax_tokens - the interactive assessment route accepts either a
messageor a clarification answer paired with aclarification_session_id - counseling requests expect a structured assessment payload and optional user signals
The default runtime uses Ollama through its OpenAI-compatible /v1/chat/completions endpoint.
- provider:
ollama - model:
gemma4:e2b - base URL:
http://127.0.0.1:11434/v1in local development - request timeout:
120seconds by default
The provider builds a chat request with:
- system prompt in Persian
- optional graph-grounded system context
- prior chat history
- current user message
The implementation raises LLMServiceError on transport errors, malformed responses, or empty completions.
The code also contains a vLLM-backed Gemma provider, but Ollama is the default and the one exercised by the current runtime configuration.
services:
backend:
ports:
- "8000:8000"
frontend:
ports:
- "3000:3000"The backend container talks to host Ollama via host.docker.internal:11434. On Linux, Compose maps that name with host-gateway.
flowchart LR
B[Browser]
FE[Frontend Container]
BE[Backend Container]
DB[(SQLite DB file)]
KG[(GraphML artifact)]
OLLAMA[Host Ollama]
B --> FE
FE --> BE
BE --> DB
BE --> KG
BE --> OLLAMA
- browser: the UI and local browser storage
- frontend container or local dev server: React/Vite app
- backend container or local dev server: FastAPI app
- host machine: Ollama default runtime
- local repository files: GraphML artifact and evaluation data/results
Persisted data lives in SQLite at the path configured by RESIGRAPH_DB_PATH.
Current tables:
usersconversationsmessagesassessmentsuser_stateclarification_sessions
- page refresh: browser
localStoragepreserves the current chat session history and theme - frontend restart: browser-stored chat history remains in the browser
- backend restart: SQLite records remain if the database file is preserved
- container restart: data remains if the DB file is volume-backed or host-mounted
The Knowledge Graph itself is static at runtime and is not written back by the application.
| Variable | Purpose | Default | Required |
|---|---|---|---|
APP_NAME |
FastAPI app title | ResiGraph |
No |
APP_ENV |
Environment label | development |
No |
APP_HOST |
Backend bind host | 0.0.0.0 |
No |
APP_PORT |
Backend bind port | 8000 |
No |
LOG_LEVEL |
Logging level | INFO |
No |
LLM_PROVIDER |
LLM provider selector | ollama |
No |
OLLAMA_BASE_URL |
Ollama base URL | http://127.0.0.1:11434/v1 |
No |
OLLAMA_MODEL |
Ollama model name | gemma4:e2b |
No |
LLM_MODEL |
LLM model fallback | gemma4:e2b |
No |
LLM_BASE_URL |
Legacy LLM base URL fallback | http://127.0.0.1:11434/v1 |
No |
LLM_REQUEST_TIMEOUT_SECONDS |
LLM request timeout | 120 |
No |
LLM_TEMPERATURE |
Sampling temperature | 0.6 |
No |
LLM_TOP_P |
Top-p sampling | 0.9 |
No |
LLM_TOP_K |
Top-k sampling | 50 |
No |
LLM_MAX_TOKENS |
Default completion limit | 512 |
No |
LLM_PRESENCE_PENALTY |
Presence penalty | 0.0 |
No |
LLM_FREQUENCY_PENALTY |
Frequency penalty | 0.0 |
No |
LLM_REPETITION_PENALTY |
Repetition penalty | 1.05 |
No |
GEMMA_MODEL_NAME |
Gemma runtime model name | gemma4:e2b |
No |
GEMMA_MAX_MODEL_LEN |
Gemma context length | 8192 |
No |
GEMMA_DTYPE |
Gemma dtype | bfloat16 |
No |
GEMMA_GPU_MEMORY_UTILIZATION |
Gemma GPU utilization | 0.9 |
No |
KNOWLEDGE_GRAPH_PATH |
GraphML artifact path | knowledge_graph/graphml/semantic_graph.graphml |
No |
CORS_ALLOWED_ORIGINS |
Allowed frontend origins | localhost dev origins | No |
CORS_ALLOWED_METHODS |
Allowed HTTP methods | GET,POST,PUT,PATCH,DELETE,OPTIONS |
No |
CORS_ALLOWED_HEADERS |
Allowed headers | * |
No |
CORS_ALLOW_CREDENTIALS |
Credentialed CORS | true |
No |
RESIGRAPH_DB_PATH |
SQLite database path | backend/data/resigraph.db |
No |
VITE_API_BASE_URL |
Browser API base URL | http://localhost:8000 in Compose |
No |
- Python 3.11+
- Node.js 20+ and npm
- Ollama running locally
- the
gemma4:e2bmodel available to Ollama, or another compatible local model configured through env vars - Docker and Docker Compose v2 for containerized usage
git clone <repository-url>
cd ResiGraphpython3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .envollama pull gemma4:e2b
ollama serveIf Ollama is already running, only the model pull step is needed.
uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reloadcd frontend
npm install
npm run dev -- --host 0.0.0.0 --port 3000docker compose build
docker compose upDetached mode:
docker compose up -dcurl http://localhost:8000/healthIf the frontend is running, open http://localhost:3000.
The evaluation framework lives under evaluation/.
- file:
evaluation/data/resilience_100.jsonl - size: 100 samples
- language: Persian
- IDs:
RES-001throughRES-100
The benchmark runner is evaluation/run_benchmark.py. It drives the live HTTP stack and records:
- per-sample predictions
- aggregate metrics
- structured error analysis
- run metadata
python3 -m evaluation.run_benchmark \
--dataset evaluation/data/resilience_100.jsonl \
--output evaluation/results/<run-name> \
--base-url http://localhost:8000 \
--ablation fullThe code currently measures:
- signal precision
- signal recall
- signal F1
- evidence grounding accuracy
- hallucinated evidence rate
- clarification required / predicted
- parse success
- validation warning count
- extracted signal count
The evaluation framework also contains contracts for KG mapping, counseling grounding, and clarification behavior in the supported ablation modes.
The latest checked-in result artifact I verified is:
evaluation/results/ollama_phase10_v4/results.json- timestamp:
2026-08-14T17:50:55Z - ablation:
full - model:
gemma4:e2b - runtime:
ollama - base URL:
http://localhost:11434/v1
Aggregate metrics from that run:
| Metric | Value |
|---|---|
signal_precision |
0.4750 |
signal_recall |
0.4617 |
signal_f1 |
0.4587 |
evidence_grounding_accuracy |
0.3567 |
hallucinated_evidence_rate |
0.0000 |
clarification_required |
0.2100 |
clarification_predicted |
1.0000 |
parse_success |
1.0000 |
validation_warning_count |
0.0000 |
extracted_signal_count |
1.8200 |
Error summary for the same run:
| Error | Count |
|---|---|
unnecessary_clarification |
79 |
missed_signal |
4 |
This is a snapshot of one checked-in run, not a claim of general performance.
The repository includes unit, integration-style, and runtime contract tests across the backend, frontend, graph, evaluation, and persistence layers.
Verified command:
pytest -qCurrent verified result:
138 passed, 8 subtests passed
Several implementation choices are visible in the code:
- GraphML as a checked-in source of truth keeps the knowledge base inspectable and reproducible.
- In-memory graph storage simplifies startup and avoids adding a graph database dependency.
- Controlled relation vocabulary reduces uncontrolled semantic drift during assessment.
- Explicit separation of score, confidence, and coverage makes uncertain assessments easier to interpret.
- SQLite keeps persistence local, lightweight, and easy to reset in development.
- A Persian-first UI and RTL layout match the intended language direction of the project.
- Ollama as the default provider supports local/offline-oriented inference.
- Clarification is bounded and deterministic rather than open-ended.
ResiGraph can operate locally with a host Ollama instance and local SQLite storage. That said, the privacy posture depends on how you deploy it:
- the browser talks to the backend over HTTP
- the backend may talk to a host model server
- conversation and profile data are persisted locally in SQLite
- the anonymous identity is stored in a cookie
The project does not claim end-to-end privacy guarantees by itself.
The current implementation has explicit limitations:
- the Knowledge Graph is static and coverage depends on the checked-in artifact
- not every relation should be read as causal
- retrieval is conservative and may miss relevant concepts if aliases do not match
- local model quality depends on the chosen runtime and model
- clarification can still be over-triggered in ambiguous cases
- assessment is structured but not clinically validated
- the benchmark is limited to the checked-in Persian dataset and current prompt/runtime behavior
Implemented today:
- GraphML loading and in-memory retrieval
- unified chat orchestration
- clarification loop
- local LLM integration
- SQLite persistence
- Persian RTL frontend
- evaluation harness
- benchmark artifacts
Future work that would be natural from the current codebase:
- stronger graph retrieval and ranking
- richer user-memory semantics
- more explicit provenance views in the UI
- broader evaluation and human annotation
- additional model backends
- tighter session management and deletion workflows
ResiGraph is best understood as an engineering and research prototype for the composition of:
- Knowledge Graph grounding
- retrieval-constrained generation
- local LLM inference
- user context persistence
- resilience assessment
The project asks a practical systems question: can a small, locally hosted model produce more structured and inspectable resilience-oriented responses when it is constrained by a domain graph, explicit assessment signals, and stored user context?
ResiGraph/
├── backend/
│ ├── api/
│ ├── assessment/
│ ├── chatbot/
│ ├── clarification/
│ ├── counseling/
│ ├── conversation/
│ ├── database/
│ ├── graph/
│ ├── llm/
│ ├── memory/
│ ├── resilience/
│ └── users/
├── frontend/
│ └── src/
├── evaluation/
│ ├── data/
│ └── results/
├── knowledge_graph/
│ └── graphml/
├── docs/
└── tests/
If you use ResiGraph in research, cite the project and the associated manuscript draft in paper/ once the publication metadata is finalized.