Metadata-Version: 2.4
Name: prashflow
Version: 1.3.0
Summary: General-purpose AI application and agent runtime with chat, RAG, tools, MCP, memory, streaming and multi-provider LLM support.
Author: Prasanth
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: pydantic>=2.7
Requires-Dist: pydantic-settings>=2.5
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.32
Requires-Dist: sqlalchemy>=2.0
Requires-Dist: langchain-core>=0.3
Requires-Dist: langchain-community>=0.3
Requires-Dist: langchain-text-splitters>=0.3
Requires-Dist: langchain-chroma>=0.2
Requires-Dist: langgraph>=0.2
Requires-Dist: langchain-ollama>=0.2
Requires-Dist: chromadb>=0.5
Requires-Dist: rank-bm25>=0.2.2
Requires-Dist: pypdf>=5.0
Requires-Dist: docx2txt>=0.8
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: numpy>=1.26
Provides-Extra: openai
Requires-Dist: langchain-openai>=0.3; extra == "openai"
Provides-Extra: web
Requires-Dist: ddgs>=9.0; extra == "web"
Provides-Extra: rerank
Requires-Dist: sentence-transformers>=3.0; extra == "rerank"
Provides-Extra: qdrant
Requires-Dist: qdrant-client>=1.12; extra == "qdrant"
Requires-Dist: langchain-qdrant>=0.2; extra == "qdrant"
Provides-Extra: faiss
Requires-Dist: faiss-cpu>=1.9; extra == "faiss"
Provides-Extra: pgvector
Requires-Dist: pgvector>=0.3; extra == "pgvector"
Requires-Dist: psycopg[binary]>=3.2; extra == "pgvector"
Requires-Dist: langchain-postgres>=0.0.12; extra == "pgvector"
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == "mcp"
Provides-Extra: litellm
Requires-Dist: litellm>=1.70; extra == "litellm"
Provides-Extra: mysql
Requires-Dist: pymysql>=1.1; extra == "mysql"
Provides-Extra: postgres
Requires-Dist: psycopg[binary]>=3.2; extra == "postgres"
Provides-Extra: a2a
Requires-Dist: a2a-sdk<2.0.0,>=1.1.2; extra == "a2a"
Requires-Dist: httpx>=0.27; extra == "a2a"
Requires-Dist: starlette>=0.40; extra == "a2a"
Requires-Dist: uvicorn>=0.30; extra == "a2a"
Provides-Extra: hitl
Requires-Dist: fastapi>=0.115; extra == "hitl"
Requires-Dist: uvicorn>=0.30; extra == "hitl"
Requires-Dist: streamlit>=1.40; extra == "hitl"
Provides-Extra: all
Requires-Dist: langchain-openai>=0.3; extra == "all"
Requires-Dist: ddgs>=9.0; extra == "all"
Requires-Dist: sentence-transformers>=3.0; extra == "all"
Requires-Dist: qdrant-client>=1.12; extra == "all"
Requires-Dist: langchain-qdrant>=0.2; extra == "all"
Requires-Dist: faiss-cpu>=1.9; extra == "all"
Requires-Dist: pgvector>=0.3; extra == "all"
Requires-Dist: psycopg[binary]>=3.2; extra == "all"
Requires-Dist: langchain-postgres>=0.0.12; extra == "all"
Requires-Dist: mcp>=1.0; extra == "all"
Requires-Dist: pymysql>=1.1; extra == "all"
Requires-Dist: litellm>=1.70; extra == "all"
Requires-Dist: a2a-sdk<2.0.0,>=1.1.2; extra == "all"
Requires-Dist: httpx>=0.27; extra == "all"
Requires-Dist: starlette>=0.40; extra == "all"
Requires-Dist: uvicorn>=0.30; extra == "all"
Requires-Dist: fastapi>=0.115; extra == "all"
Requires-Dist: streamlit>=1.40; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: ruff>=0.8; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=6.0; extra == "dev"

# PrashFlow

**PrashFlow** is a general-purpose Python AI application and agent runtime designed to make Chat, RAG, agents, multi-agent systems, tools, MCP, memory, streaming, and multiple model providers available through a small Python API.

It is **not only a RAG library**.

## What you can build

- Local Ollama RAG applications
- Streaming chat with user sessions
- Semantic, BM25/keyword, hybrid, MMR and multi-query retrieval
- Chroma, Qdrant, FAISS, PGVector and in-memory vector stores
- PDF/DOCX/TXT/Markdown/CSV/web ingestion
- SQL database ingestion
- Single tool-using agents with LangGraph
- Agentic Ollama chat
- Supervisor, sequential and parallel multi-agent workflows
- Custom Python tools
- Optional web search
- MCP configuration/adapter boundary
- OpenAI and OpenAI-compatible models
- LiteLLM model gateway
- Environment-variable based YAML configuration
- Retries and clear validation errors

## Architecture

```text
                         PRASHFLOW
                             |
       +---------------------+----------------------+
       |                     |                      |
      Chat                   RAG                  Agent
       |                     |                      |
   Streaming           Ingestion/Retrieval      Tools
   Sessions                  |                    MCP
       |              +-------+-------+             |
       |              |       |       |             |
       |           Semantic  BM25   MMR             |
       |              |       |       |             |
       |              +-------+-------+             |
       |                      |                     |
       |                     RRF                    |
       |                      |                     |
       |                   Rerank                   |
       |                      |                     |
       +----------------------+---------------------+
                              |
                         Model Layer
                              |
                    +---------+---------+
                    |         |         |
                  Ollama    OpenAI    LiteLLM
                              |
                         MultiAgent
                              |
                 +------------+------------+
                 |            |            |
             Supervisor   Sequential    Parallel
```

## Installation

Basic:

```bash
pip install prashflow
```

All optional integrations:

```bash
pip install "prashflow[all]"
```

For local Ollama + Chroma RAG, the `all` extra is convenient. You can also install only the extras you need.

## 1. Local Ollama RAG in a few lines

Put documents in `./knowledge`:

```text
my-app/
├── knowledge/
│   ├── deployment.pdf
│   ├── architecture.docx
│   ├── troubleshooting.txt
│   └── security.md
├── data/
└── app.py
```

Pull local models:

```bash
ollama pull qwen3:8b
ollama pull nomic-embed-text
```

Python:

```python
from prashflow import RAG

rag = RAG(
    llm="ollama:qwen3:8b",
    embeddings="ollama:nomic-embed-text",
    vector_db="chroma:./data/chroma",
    retrieval={
        "type": "hybrid",
        "top_k": 5,
        "candidate_k": 20,
        "semantic_weight": 0.6,
        "keyword_weight": 0.4,
    },
)

rag.ingest("./knowledge")

print(rag.ask("What is our production deployment process?"))
```

PrashFlow hides the LangChain, Chroma, loader and embedding implementation from the application code.

## 2. Search algorithms

Semantic:

```python
rag.search("production deployment", search_type="semantic")
```

BM25/keyword:

```python
rag.search("JIRA-12345", search_type="keyword")
```

MMR:

```python
rag.search("deployment architecture", search_type="mmr")
```

Hybrid:

```python
rag.search("production deployment", search_type="hybrid")
```

Multi-query:

```python
rag.search("How do we release an application?", search_type="multi_query")
```

Hybrid combines semantic and keyword rankings with reciprocal-rank fusion. MMR adds diversity. An optional cross-encoder reranker can be enabled with the `rerank` extra.

## 3. Persistent Chroma

```python
rag = RAG(
    llm="ollama:qwen3:8b",
    embeddings="ollama:nomic-embed-text",
    vector_db={
        "provider": "chroma",
        "path": "./data/chroma",
        "collection": "company_docs",
    },
)
```

The Chroma data remains on disk after the Python process exits.

### In-memory vector store

```python
rag = RAG(
    llm="ollama:qwen3:8b",
    embeddings="ollama:nomic-embed-text",
    vector_db={"provider": "memory"},
)
```

This is intended for tests, demos and short-lived applications.

## 4. User session + streaming RAG chat

This is the recommended API for a local RAG chatbot:

```python
from prashflow import RAG

rag = RAG(
    llm="ollama:qwen3:8b",
    embeddings="ollama:nomic-embed-text",
    vector_db="chroma:./data/chroma",
    retrieval={"type": "hybrid", "top_k": 5},
)

rag.ingest("./knowledge")

session_id = "user-001"

while True:
    question = input("You: ")
    if question.lower() in {"exit", "quit"}:
        break

    print("AI: ", end="")
    for token in rag.chat_stream(
        session_id=session_id,
        query=question,
        search_type="hybrid",
    ):
        print(token, end="", flush=True)
    print()
```

The session stores conversation history independently for each `session_id`.

```text
user-001 -> conversation A
user-002 -> conversation B
user-003 -> conversation C
```

The default session backend is in-memory. A persistent Redis/PostgreSQL session backend can be added behind the same `SessionStore` abstraction.

## 5. Normal Chat

```python
from prashflow import Chat

chat = Chat(
    llm="ollama:qwen3:8b",
    session_id="user-001",
)

print(chat.chat("My name is Prash."))
print(chat.chat("What is my name?"))
```

Streaming:

```python
for token in chat.stream("Explain Kubernetes"):
    print(token, end="", flush=True)
```

## 6. Agentic Ollama Chat

Use `AgentChat` when the model should decide when to call tools.

```python
from prashflow import AgentChat

agent = AgentChat(
    llm="ollama:qwen3:8b",
    tools=["calculator"],
    session_id="user-001",
)

for token in agent.stream("Calculate 25% of 8000"):
    print(token, end="", flush=True)
```

PrashFlow uses LangGraph internally for the agent loop. The application does not need to build `StateGraph` or `ToolNode` itself.

## 7. Custom Python tools

```python
from prashflow import Agent
def get_server_status(server: str) -> str:
    """Get Linux server status."""
    return f"{server}: UP"
agent = Agent(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
        "base_url": "http://localhost:11434",
    },
    tools=[
        "calculator",
        get_server_status,
    ],
)
print(
    agent.run(
        "Check web01 status and calculate 20 percent of 500."
    )
)
```

Tools can also require interactive approval:

```python
@tool(requires_approval=True, max_retries=2)
def restart_service(server: str, service: str) -> str:
    """Restart a Linux service."""
    # implement the real operation here
    return f"Restarted {service} on {server}"
```

## 8. Multi-agent

### Supervisor

```python
from prashflow import MultiAgent

team = MultiAgent(
    model="ollama:qwen3:8b",
    mode="supervisor",
    agents=[
        {
            "name": "researcher",
            "description": "Research technical information.",
            "tools": ["web_search"],
        },
        {
            "name": "calculator",
            "description": "Perform arithmetic calculations.",
            "tools": ["calculator"],
        },
    ],
)

print(team.run("Calculate 20% of 8000"))
```

The supervisor chooses the specialist.

### Sequential

```python
team = MultiAgent(
    model="ollama:qwen3:8b",
    mode="sequential",
    agents=[
        {"name": "planner", "description": "Create a plan."},
        {"name": "developer", "description": "Develop the solution."},
        {"name": "reviewer", "description": "Review the solution."},
    ],
)
```

Flow:

```text
Planner -> Developer -> Reviewer -> Final
```

### Parallel

```python
team = MultiAgent(
    model="ollama:qwen3:8b",
    mode="parallel",
    agents=[
        {"name": "security", "description": "Analyze security."},
        {"name": "performance", "description": "Analyze performance."},
        {"name": "architecture", "description": "Analyze architecture."},
    ],
)
```

The current reference implementation runs the specialist calls independently and synthesizes their results. An async concurrent implementation can be added for high-throughput production workloads.

### Streaming multi-agent

```python
for token in team.stream("Analyze this deployment"):
    print(token, end="", flush=True)
```

### Per-agent models

```python
team = MultiAgent(
    model="ollama:qwen3:8b",
    agents=[
        {
            "name": "researcher",
            "model": "ollama:qwen3:8b",
            "description": "Research information.",
        },
        {
            "name": "coder",
            "model": "ollama:qwen2.5-coder:14b",
            "description": "Write and review code.",
        },
    ],
)
```

## 9. RAG + Agent

```python
from prashflow import AgentChat, RAG

rag = RAG(
    llm="ollama:qwen3:8b",
    embeddings="ollama:nomic-embed-text",
    vector_db="chroma:./data/chroma",
    retrieval={"type": "hybrid", "top_k": 5},
)
rag.ingest("./knowledge")

agent = AgentChat(
    llm="ollama:qwen3:8b",
    tools=[rag.as_tool(), "calculator"],
)

print(agent.run("Find our production deployment procedure."))
```

## 10. Multi-agent + RAG

```python
team = MultiAgent(
    model="ollama:qwen3:8b",
    mode="supervisor",
    agents=[
        {
            "name": "company_knowledge",
            "description": "Answer questions from company documents.",
            "tools": [rag.as_tool()],
        },
        {
            "name": "calculator",
            "description": "Perform calculations.",
            "tools": ["calculator"],
        },
    ],
)
```

## 11. SQL ingestion

```python
rag.ingest_sql(
    url="postgresql+psycopg://user:password@localhost:5432/company",
    query="SELECT id, title, description FROM incidents",
    content_columns=["title", "description"],
    metadata_columns=["id"],
)
```

MySQL is supported through the SQLAlchemy connection URL when the MySQL extra is installed.

## 12. MCP

PrashFlow provides an MCP configuration boundary so MCP can be attached to agents without changing the agent API.

```python
agent = AgentChat(
    llm="ollama:qwen3:8b",
    mcp_servers=[
        {
            "name": "filesystem",
            "transport": "stdio",
            "command": "npx",
            "args": ["-y", "@modelcontextprotocol/server-filesystem", "./workspace"],
        }
    ],
)
```

The reference package validates MCP server configuration. For production MCP transport/session discovery, pin and implement against the MCP SDK version used by your organization; MCP SDK transport APIs can evolve.

## 13. LiteLLM

```python
from prashflow import Chat

chat = Chat(
    llm={
        "provider": "litellm",
        "model": "openai/gpt-4.1",
    }
)
```

The application API remains `chat.chat()` / `chat.stream()` while the provider is selected by LiteLLM.

## 14. OpenAI / OpenAI-compatible

```python
from prashflow import Chat

chat = Chat(
    llm={
        "provider": "openai-compatible",
        "model": "my-model",
        "base_url": "http://localhost:8000/v1",
        "api_key": "dummy",
    }
)
```

## 15. YAML configuration

`prashflow.yaml`:

```yaml
llm:
  provider: ollama
  model: qwen3:8b
  base_url: http://localhost:11434

embeddings:
  provider: ollama
  model: nomic-embed-text
  base_url: http://localhost:11434

vector_db:
  provider: chroma
  path: ./data/chroma
  collection: company_docs

retrieval:
  type: hybrid
  top_k: 5
  candidate_k: 20
  semantic_weight: 0.6
  keyword_weight: 0.4

chunking:
  size: 1000
  overlap: 200

reranker:
  enabled: false
```

Load it:

```python
from prashflow import RAG

rag = RAG.from_config("prashflow.yaml")
```

Environment variables are supported:

```yaml
llm:
  provider: openai
  model: ${OPENAI_MODEL}
  api_key: ${OPENAI_API_KEY}
```

## 16. Error handling

PrashFlow exposes typed exceptions:

```python
from prashflow import PrashFlowError

try:
    print(rag.ask("What is our deployment process?"))
except PrashFlowError as exc:
    print(f"PrashFlow error: {exc}")
```

Available categories include configuration, LLM, embedding, vector DB, document loading, retrieval, reranking, tool, agent and MCP errors.


## 17. Design philosophy

Application developers should write:

```python
from prashflow import RAG, AgentChat, MultiAgent
```

and should not need to directly assemble LangChain loaders, LangGraph state graphs, Chroma clients, BM25 indexes, tool nodes or model-provider adapters for common use cases.

Advanced developers can still customize the underlying components when needed.

## Roadmap

Planned production enhancements:

- true async/parallel specialist execution
- persistent Redis/PostgreSQL session backends
- complete MCP client/session discovery against a pinned SDK
- A2A support
- model fallback and cost routing
- observability/tracing
- structured output and Pydantic schemas
- FastAPI integration helpers
- ingestion manifests and changed-file detection
- background ingestion jobs
- citation objects with source/page metadata



## PrashFlow 1.2 — Runtime Params, Token Usage, Reasoning and A2A

P
### Params

```python
from prashflow import RAG, Params

params = Params(
    context_window=32768,
    max_context_tokens=12000,
    reserve_output_tokens=2000,
    thinking_level="high",
    top_k=8,
    search_type="mmr",
    max_input_tokens=12000,
    max_output_tokens=2000,
    max_total_tokens=14000,
    context_strategy="relevance",
    track_usage=True,
)

rag = RAG(
    llm={"provider": "ollama", "model": "qwen3:8b"},
    embeddings={"provider": "ollama", "model": "nomic-embed-text"},
    vector_db={"provider": "chroma", "path": "./chroma_db"},
    params=params,
)
```

### Backward-compatible result API

Existing code remains:

```python
answer = rag.ask("What is our deployment process?")
```

and returns a string.

For token/context/source metadata:

```python
result = rag.ask_result("What is our deployment process?")

print(result.answer)
print(result.usage.input_tokens)
print(result.usage.output_tokens)
print(result.usage.total_tokens)
print(result.usage.context_tokens)
print(result.usage.utilization)
print(result.thinking_level)
```

Provider usage is preferred. If a provider does not return usage metadata,
PrashFlow uses an estimate and marks `result.usage.estimated == True`.

### Thinking levels

```text
off
low
medium
high
auto
```

These are translated to provider-specific settings where supported. PrashFlow
does not expose or store private chain-of-thought.

### A2A

A2A is optional:

```bash
pip install "prashflow[a2a]"
```

Client:

```python
from prashflow import A2AClient

agent = A2AClient(
    "http://localhost:9001/",
    name="security",
)

await agent.connect()
result = await agent.ask(
    "Analyze this architecture."
)
print(result.answer)
await agent.close()
```

Multiple remote agents:

```python
from prashflow import A2ARegistry, A2AOrchestrator

registry = A2ARegistry({
    "security": "http://localhost:9001/",
    "devops": "http://localhost:9002/",
    "database": "http://localhost:9003/",
})

await registry.connect_all()

orchestrator = A2AOrchestrator(registry)

results = await orchestrator.broadcast(
    "Analyze this production architecture.",
    agents=["security", "devops", "database"],
)
```

Expose a PrashFlow RAG/Agent runtime:

```python
from prashflow import run_a2a_server

run_a2a_server(
    rag,
    host="0.0.0.0",
    port=9999,
    name="PrashFlow RAG Agent",
)
```

A2A uses the official `a2a-sdk` Python client/server abstractions and
Agent Card discovery.



### Model routing

A single model remains fully supported:

```python
llm={"provider": "ollama", "model": "qwen3:8b"}
```

For multiple models:

```python
llm={
    "strategy": "priority",
    "models": [
        {"provider": "ollama", "model": "qwen3:8b"},
        {"provider": "ollama", "model": "llama3.1:8b"},
        {"provider": "openai", "model": "gpt-5"},
    ],
    "fallback_on_error": True,
}
```

Supported routing strategies:

```text
priority        first model first; fallback on failure
fallback        priority/fallback behavior
round_robin     rotate the starting model
balanced        prefer models with fewer recent failures
least_failures  same failure-aware policy
first_available priority with health checks
```

### Automatic fallback

Fallback happens when the current provider/model raises an exception.

```python
rag = RAG(
    llm={
        "strategy": "priority",
        "models": [
            {"provider": "ollama", "model": "qwen3:8b"},
            {"provider": "ollama", "model": "llama3.1:8b"},
        ],
    }
)

answer = rag.ask("Explain our deployment process.")
```

Existing single-model applications are unchanged.

### Health check

```python
status = rag.health_check()
print(status)
```

Deep check:

```python
status = rag.health_check(deep=True)
```

The deep check verifies LLM endpoint availability, embeddings and vector-store
access. The normal check is intentionally lightweight.

For a routed model directly:

```python
from prashflow import ModelRouter

router = ModelRouter([
    {"provider": "ollama", "model": "qwen3:8b"},
    {"provider": "ollama", "model": "llama3.1:8b"},
])

print(router.health_check())
```

### Health-aware routing

To check a model before every request:

```python
params = Params(
    health_check_before_request=True,
    health_check_timeout=3,
)

rag = RAG(
    llm={
        "strategy": "priority",
        "models": [
            {"provider": "ollama", "model": "qwen3:8b"},
            {"provider": "ollama", "model": "llama3.1:8b"},
        ],
    },
    params=params,
)
```

This is more expensive than normal fallback, so it is opt-in.

### RAG evaluation

PrashFlow includes a dependency-free regression evaluator:

```python
evaluation = rag.evaluate([
    {
        "question": "Who approves production deployments?",
        "expected_answer": "The DevOps team approves production deployments.",
        "expected_sources": ["deployment.md"],
    },
    {
        "question": "What is the rollback procedure?",
        "expected_answer": "Rollback is performed using the previous release.",
    },
])

print(evaluation["averages"])
```

Metrics:

```text
context_relevance
context_recall
answer_relevance
faithfulness
source_recall
overall
```

These are transparent lexical/coverage heuristics designed for regression
testing. They are not a replacement for a semantic evaluator such as RAGAS
or an LLM-as-a-judge system.

###
# PrashFlow — End User Guide

> **For users who install PrashFlow with `pip` and want to build AI/RAG applications.**
>
> You do **not** need to clone the PrashFlow repository or understand its internal files to use this guide.

---

## 1. What is PrashFlow?

PrashFlow is a Python framework that gives you a simple API for building:

- RAG applications
- LLM applications
- AI agents
- Multi-agent applications
- MCP integrations
- A2A agent integrations
- Model routing
- Automatic model fallback
- Context/token management
- Thinking/reasoning configuration
- Usage tracking
- Health checks
- RAG evaluation

The goal is to let you configure your AI infrastructure instead of writing the orchestration yourself.

Typical application:

```text
Your Python Application
        |
        v
     PrashFlow
        |
   +----+----------------------+
   |    |          |           |
   v    v          v           v
  RAG  Agent      MCP         A2A
   |
   v
Model Router
   |
+--+-----------+-----------+
|              |           |
Ollama       OpenAI      LiteLLM
   |
   v
Vector Database
   |
   +--> Chroma
   +--> other supported stores
```

---

# 2. Install PrashFlow

You install PrashFlow like any normal Python package.

```bash
pip install prashflow
```

Verify:

```bash
python -c "import prashflow; print(prashflow.__version__)"
```

You should see the installed version.

---

# 3. Optional A2A Installation

If you want A2A support:

```bash
pip install "prashflow[a2a]"
```

If you only need RAG/LLM functionality, the base installation is sufficient.

---

# 4. Optional Provider Dependencies

PrashFlow uses provider integrations.

For example, if you want to use Ollama, install/run Ollama separately and pull the models you need.

Example:

```bash
ollama pull qwen3:8b
ollama pull nomic-embed-text
```

Check:

```bash
ollama list
```

---

# 5. Your First PrashFlow Application

Create:

```text
app.py
```

Add:

```python
from prashflow import RAG

rag = RAG(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },
    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },
    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },
)

answer = rag.ask("What is PrashFlow?")

print(answer)
```

Run:

```bash
python app.py
```

That's the basic PrashFlow application.

---

# 6. What Do I Need to Configure?

For a basic RAG application, you normally provide:

```text
LLM
Embedding Model
Vector Database
Prompt
Optional Params
```

Example:

```python
rag = RAG(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },
)
```

PrashFlow handles the orchestration.

---

# 7. LLM Configuration

## Ollama

```python
llm={
    "provider": "ollama",
    "model": "qwen3:8b",
}
```

Custom Ollama endpoint:

```python
llm={
    "provider": "ollama",
    "model": "qwen3:8b",
    "base_url": "http://localhost:11434",
}
```

For a remote Ollama server:

```python
llm={
    "provider": "ollama",
    "model": "qwen3:8b",
    "base_url": "http://192.168.1.100:11434",
}
```

---

## OpenAI

```python
llm={
    "provider": "openai",
    "model": "gpt-5",
    "api_key": "YOUR_API_KEY",
}
```

For production applications, do not hard-code secrets.

Use environment variables or a secret manager.

---

## LiteLLM

```python
llm={
    "provider": "litellm",
    "model": "openai/gpt-5",
}
```

The exact model name and provider configuration depend on your LiteLLM setup.

---

# 8. Embedding Configuration

Embeddings convert your documents and user queries into vectors.

Example:

```python
embeddings={
    "provider": "ollama",
    "model": "nomic-embed-text",
}
```

Conceptually:

```text
Document
   |
   v
Embedding Model
   |
   v
Vector
   |
   v
Vector Database
```

Use a compatible and consistent embedding model when indexing and querying the same collection.

---

# 9. Vector Database

## Chroma

The simplest local option:

```python
vector_db={
    "provider": "chroma",
    "path": "./chroma_db",
}
```

This creates/uses a local Chroma database.

Your application can therefore look like:

```text
my-ai-app/
├── app.py
└── chroma_db/
```

---

# 10. Complete Basic RAG Configuration

```python
from prashflow import RAG

rag = RAG(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },
)

answer = rag.ask(
    "Explain our deployment process."
)

print(answer)
```

---

# 11. Using Params

Advanced behavior is configured through `Params`.

```python
from prashflow import RAG, Params

params = Params(
    context_window=32768,
    max_context_tokens=12000,
    reserve_output_tokens=2000,

    thinking_level="medium",

    top_k=8,
    search_type="mmr",

    max_input_tokens=12000,
    max_output_tokens=2000,
    max_total_tokens=14000,

    context_strategy="relevance",
    track_usage=True,
)

rag = RAG(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },
    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },
    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },
    params=params,
)
```

---

# 12. Params Explained

## `context_window`

```python
context_window=32768
```

The model's configured context capacity.

Example:

```text
32,768 tokens
```

means the model can work with a context budget of approximately 32K tokens, subject to the actual provider/model capabilities.

---

## `max_context_tokens`

```python
max_context_tokens=12000
```

Maximum amount of retrieved context that PrashFlow should attempt to place into the prompt.

This prevents retrieval from consuming the entire model context.

---

## `reserve_output_tokens`

```python
reserve_output_tokens=2000
```

Reserves space for the generated response.

Conceptually:

```text
Model Context
+-----------------------------------+
| System / user / history           |
|                                   |
| Retrieved Context                 |
| max_context_tokens                |
|                                   |
| Reserved Output                   |
| reserve_output_tokens             |
+-----------------------------------+
```

---

## `max_input_tokens`

```python
max_input_tokens=12000
```

Maximum input-token budget.

---

## `max_output_tokens`

```python
max_output_tokens=2000
```

Maximum output-token budget.

---

## `max_total_tokens`

```python
max_total_tokens=14000
```

Maximum combined input/output budget.

---

## `top_k`

```python
top_k=8
```

Number of documents/results retrieved.

Higher values can improve recall but may increase:

- latency
- context size
- token consumption
- irrelevant context

Start with:

```python
top_k=5
```

or:

```python
top_k=8
```

and evaluate.

---

## `search_type`

Example:

```python
search_type="mmr"
```

MMR can help balance relevance and diversity.

---

## `context_strategy`

Example:

```python
context_strategy="relevance"
```

Controls how PrashFlow manages retrieved context.

---

## `track_usage`

```python
track_usage=True
```

Enables token/usage metadata.

---

# 13. Thinking Levels

PrashFlow supports:

```python
thinking_level="off"
thinking_level="low"
thinking_level="medium"
thinking_level="high"
thinking_level="auto"
```

---

## Off

```python
Params(
    thinking_level="off"
)
```

Use for simple tasks where reasoning overhead is unnecessary.

Examples:

```text
classification
simple extraction
simple formatting
simple Q&A
```

---

## Low

```python
Params(
    thinking_level="low"
)
```

Good for normal questions and simple RAG.

---

## Medium

```python
Params(
    thinking_level="medium"
)
```

Good default for most applications.

---

## High

```python
Params(
    thinking_level="high"
)
```

Useful for:

```text
complex reasoning
architecture analysis
debugging
multi-step questions
complex coding
```

Higher reasoning can increase latency and token consumption.

---

## Auto

```python
Params(
    thinking_level="auto"
)
```

Allows the model/provider integration to use its default behavior.

---

## Important

Thinking level is a runtime policy.

PrashFlow does not expose private chain-of-thought.

You receive:

```text
Final Answer
+
Usage Metadata
+
Model Metadata
```

not hidden reasoning.

Actual thinking support depends on the selected model/provider.

---

# 14. Token Usage

Instead of:

```python
answer = rag.ask(...)
```

use:

```python
result = rag.ask_result(
    "Explain our deployment architecture."
)
```

Then:

```python
print(result.answer)
```

Usage:

```python
print("Input:", result.usage.input_tokens)
print("Output:", result.usage.output_tokens)
print("Total:", result.usage.total_tokens)
print("Context:", result.usage.context_tokens)
print("Utilization:", result.usage.utilization)
print("Estimated:", result.usage.estimated)
```

---

# 15. `ask()` vs `ask_result()`

## Existing/simple API

```python
answer = rag.ask("What is PrashFlow?")
```

Returns:

```python
str
```

## Metadata API

```python
result = rag.ask_result(
    "What is PrashFlow?"
)
```

Returns a result object containing:

```text
answer
sources
usage
thinking_level
provider
model
context
metadata
```

This means existing applications can continue using:

```python
rag.ask(...)
```

without changing their code.

---

# 16. Token Utilization

Suppose:

```python
context_window=32768
```

and total usage is:

```text
8192 tokens
```

The utilization is approximately:

```text
25%
```

Use:

```python
print(result.usage.utilization)
```

Provider-reported usage is preferred.

If the provider does not return usage information, PrashFlow can estimate usage.

Check:

```python
print(result.usage.estimated)
```

---

# 17. Custom Prompt

You can provide a prompt:

```python
rag = RAG(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },

    prompt="""
    You are an enterprise knowledge assistant.

    Answer only using the retrieved context.

    If the answer is not present in the context,
    say that you do not know.

    Context:
    {context}

    Question:
    {question}

    History:
    {history}
    """,
)
```

Common placeholders:

```text
{context}
{question}
{history}
```

Retrieved documents should be treated as data, not as system-level instructions.

---

# 18. Ingesting Documents

The normal RAG pipeline is:

```text
Documents
   |
   v
Loader
   |
   v
Chunks
   |
   v
Embeddings
   |
   v
Chroma
```

Use the ingestion/loader APIs exposed by your installed PrashFlow version.

After ingestion, users can query:

```python
answer = rag.ask(
    "What does the deployment document say?"
)
```

---

# 19. Retrieval Settings

Example:

```python
params = Params(
    top_k=8,
    search_type="mmr",
)
```

Tune retrieval based on your data.

A common starting point:

```python
Params(
    top_k=5,
    search_type="mmr",
)
```

Then compare results using the evaluation functionality.

---

# 20. Streaming

For applications where you want output as it is generated:

```python
for chunk in rag.ask_stream(
    "Explain Kubernetes architecture."
):
    print(chunk, end="", flush=True)
```

Useful for:

- chat UIs
- web applications
- long responses
- interactive assistants

---

# 21. Model Routing

You can configure multiple LLMs.

```python
rag = RAG(
    llm={
        "strategy": "priority",

        "models": [
            {
                "provider": "ollama",
                "model": "qwen3:8b",
            },

            {
                "provider": "ollama",
                "model": "llama3.1:8b",
            },

            {
                "provider": "openai",
                "model": "gpt-5",
                "api_key": "YOUR_API_KEY",
            },
        ],

        "fallback_on_error": True,
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },
)
```

---

# 22. Routing Strategies

## Priority

```python
strategy="priority"
```

Models are tried in the order provided.

```text
Model 1
  |
  X
  |
Model 2
  |
  X
  |
Model 3
```

---

## Fallback

```python
strategy="fallback"
```

Uses priority/fallback behavior.

---

## Round Robin

```python
strategy="round_robin"
```

Rotates the starting model.

Useful when multiple models are equivalent and you want to distribute requests.

---

## Balanced

```python
strategy="balanced"
```

Prefers models with fewer recent failures.

---

## Least Failures

```python
strategy="least_failures"
```

Uses recent failure counts when deciding which model to use.

---

## First Available

```python
strategy="first_available"
```

Useful with health-aware routing.

---

# 23. Automatic Fallback

Example:

```python
llm={
    "strategy": "priority",

    "models": [
        {
            "provider": "ollama",
            "model": "qwen3:8b",
        },

        {
            "provider": "ollama",
            "model": "llama3.1:8b",
        },
    ],

    "fallback_on_error": True,
}
```

If the first model fails:

```text
qwen3:8b
     |
     X
     |
     v
llama3.1:8b
     |
     v
Response
```

You can disable fallback:

```python
"fallback_on_error": False
```

---

# 24. Testing Fallback

For testing, intentionally configure an invalid first model:

```python
llm={
    "strategy": "priority",

    "models": [
        {
            "provider": "ollama",
            "model": "model-that-does-not-exist",
        },

        {
            "provider": "ollama",
            "model": "qwen3:8b",
        },
    ],

    "fallback_on_error": True,
}
```

Then:

```python
answer = rag.ask(
    "Explain PrashFlow."
)

print(answer)
```

Expected:

```text
Invalid model
     |
   error
     |
     v
qwen3:8b
     |
     v
answer
```

---

# 25. Health Check

Basic:

```python
status = rag.health_check()

print(status)
```

Deep:

```python
status = rag.health_check(
    deep=True
)

print(status)
```

Deep checking verifies the important RAG components:

```text
LLM
 |
Embeddings
 |
Vector DB
```

Example:

```python
{
    "healthy": True,
    "llm": {
        "healthy": True,
    },
    "embeddings": {
        "healthy": True,
    },
    "vector_db": {
        "healthy": True,
    },
}
```

---

# 26. Model Router Health Check

You can also directly use:

```python
from prashflow import ModelRouter

router = ModelRouter([
    {
        "provider": "ollama",
        "model": "qwen3:8b",
    },
    {
        "provider": "ollama",
        "model": "llama3.1:8b",
    },
])

status = router.health_check()

print(status)
```

The result contains individual model health information.

---

# 27. Health Check Before Every Request

You can enable:

```python
from prashflow import Params

params = Params(
    health_check_before_request=True,
    health_check_timeout=3,
)
```

Then:

```python
rag = RAG(
    llm={
        "strategy": "priority",
        "models": [
            {
                "provider": "ollama",
                "model": "qwen3:8b",
            },
            {
                "provider": "ollama",
                "model": "llama3.1:8b",
            },
        ],
    },
    params=params,
)
```

This performs an additional health operation before selecting a model.

For high-throughput applications, explicit health endpoints plus fallback are generally more efficient than checking on every request.

---

# 28. RAG Evaluation

PrashFlow includes a built-in evaluator for RAG regression testing.

Example:

```python
evaluation = rag.evaluate([
    {
        "question": "Who approves production deployments?",

        "expected_answer": (
            "The DevOps team approves production deployments."
        ),

        "expected_sources": [
            "deployment.md"
        ],
    },

    {
        "question": "What is used for CI/CD?",

        "expected_answer": (
            "Jenkins is used for CI/CD."
        ),
    },
])
```

Print:

```python
print(evaluation["averages"])
```

---

# 29. Evaluation Metrics

PrashFlow reports:

```text
context_relevance
context_recall
answer_relevance
faithfulness
source_recall
overall
```

Example:

```python
{
    "context_relevance": 0.91,
    "context_recall": 0.88,
    "answer_relevance": 0.94,
    "faithfulness": 0.90,
    "source_recall": 1.0,
    "overall": 0.926,
}
```

Per-question:

```python
for item in evaluation["results"]:
    print(item["question"])
    print(item["metrics"])
```

These are transparent, dependency-free lexical/coverage heuristics.

They are useful for:

- regression tests
- comparing retrieval settings
- CI validation
- tuning RAG

They are not a complete semantic evaluation system.

---

# 30. Compare Two RAG Configurations

For example, test:

```python
top_k=5
```

versus:

```python
top_k=10
```

Run the same evaluation dataset.

Example:

```text
Configuration A

Overall: 0.84


Configuration B

Overall: 0.91
```

This gives you a practical way to measure RAG changes.

---

# 31. Agents

Create an agent:

```python
from prashflow import Agent

agent = Agent(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },
)
```

Then use the existing Agent API:

```python
answer = agent.run(
    "Explain Kubernetes deployments."
)

print(answer)
```

Agents are useful when you need:

```text
LLM
+
Tools
+
Decision making
+
Memory
```

---

# 32. AgentChat

```python
from prashflow import AgentChat

chat = AgentChat(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },
)
```

Use the conversational methods exposed by your installed PrashFlow version.

---

# 33. Multi-Agent

PrashFlow supports multi-agent applications.

Concept:

```text
                 User Request
                      |
                      v
                Multi-Agent
                      |
          +-----------+-----------+
          |           |           |
          v           v           v
       Security     DevOps     Database
        Agent        Agent       Agent
          |           |           |
          +-----------+-----------+
                      |
                      v
                 Final Result
```

Use the `MultiAgent` API included in your installed version.

---

# 34. Tools

PrashFlow provides tools:

```python
from prashflow import tool, ToolManager
```

Tools allow agents to interact with external functionality.

Examples:

```text
Python functions
REST APIs
Databases
MCP tools
RAG
A2A agents
```

---

# 35. MCP

PrashFlow supports MCP.

Conceptually:

```text
PrashFlow Agent
      |
      v
   MCP Client
      |
  +---+---+---+
  |   |   |   |
 Git DB  API Files
```

Use the MCP APIs available in your installed version.

---

# 36. A2A

A2A allows PrashFlow agents to communicate with remote agents.

Install:

```bash
pip install "prashflow[a2a]"
```

Example:

```python
from prashflow import A2AClient

agent = A2AClient(
    "http://localhost:9001/",
    name="security-agent",
)
```

Connect:

```python
await agent.connect()
```

Send a request:

```python
result = await agent.ask(
    "Analyze this architecture for security risks."
)

print(result.answer)
```

Close:

```python
await agent.close()
```

---

# 37. Multiple A2A Agents

Create a registry:

```python
from prashflow import A2ARegistry

registry = A2ARegistry({
    "security": "http://localhost:9001/",
    "devops": "http://localhost:9002/",
    "database": "http://localhost:9003/",
})
```

Connect:

```python
await registry.connect_all()
```

List:

```python
print(registry.names())
```

---

# 38. A2A Skill Routing

Search for agents by skill:

```python
matches = registry.find_by_skill(
    "security"
)
```

Use `A2ARouter`:

```python
from prashflow import A2ARouter

router = A2ARouter(registry)

result = await router.ask(
    "Check this deployment for security risks.",
    skill="security",
)

print(result.answer)
```

Explicit agent:

```python
result = await router.ask(
    "Analyze this deployment.",
    agent="security",
)
```

---

# 39. A2A Orchestration

Use multiple remote agents:

```python
from prashflow import A2AOrchestrator

orchestrator = A2AOrchestrator(registry)

results = await orchestrator.broadcast(
    "Analyze this production architecture.",

    agents=[
        "security",
        "devops",
        "database",
    ],
)
```

Flow:

```text
                  Request
                     |
                     v
              A2A Orchestrator
                     |
        +------------+------------+
        |            |            |
        v            v            v
    Security       DevOps      Database
      Agent         Agent        Agent
        |            |            |
        +------------+------------+
                     |
                     v
                  Results
```

---

# 40. A2A Server

A PrashFlow runtime can be exposed as an A2A agent.

```python
from prashflow import RAG, Params, run_a2a_server

rag = RAG(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },

    params=Params(
        thinking_level="medium",
        track_usage=True,
    ),
)

run_a2a_server(
    rag,
    host="0.0.0.0",
    port=9999,
    name="PrashFlow RAG Agent",
)
```

---

# 41. Recommended Production Configuration

```python
from prashflow import RAG, Params

params = Params(
    context_window=32768,

    max_context_tokens=12000,

    reserve_output_tokens=2000,

    thinking_level="medium",

    top_k=8,

    search_type="mmr",

    max_input_tokens=12000,

    max_output_tokens=2000,

    max_total_tokens=14000,

    context_strategy="relevance",

    track_usage=True,

    fallback_on_error=True,

    health_check_timeout=5,
)

rag = RAG(
    llm={
        "strategy": "priority",

        "models": [
            {
                "provider": "ollama",
                "model": "qwen3:8b",
            },

            {
                "provider": "ollama",
                "model": "llama3.1:8b",
            },
        ],

        "fallback_on_error": True,
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },

    params=params,
)
```

---

# 42. Complete End-User Example

Create:

```text
app.py
```

```python
from prashflow import RAG, Params


params = Params(
    context_window=32768,

    max_context_tokens=12000,

    reserve_output_tokens=2000,

    thinking_level="medium",

    top_k=8,

    search_type="mmr",

    max_input_tokens=12000,

    max_output_tokens=2000,

    max_total_tokens=14000,

    context_strategy="relevance",

    track_usage=True,

    fallback_on_error=True,
)


rag = RAG(
    llm={
        "strategy": "priority",

        "models": [
            {
                "provider": "ollama",
                "model": "qwen3:8b",
            },

            {
                "provider": "ollama",
                "model": "llama3.1:8b",
            },
        ],

        "fallback_on_error": True,
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },

    params=params,
)


# -------------------------
# Health
# -------------------------

health = rag.health_check()

print("HEALTH")
print(health)


# -------------------------
# Ask
# -------------------------

result = rag.ask_result(
    "Explain our production deployment process."
)


print("\nANSWER")
print(result.answer)


# -------------------------
# Usage
# -------------------------

if result.usage:

    print("\nUSAGE")

    print(
        "Input tokens:",
        result.usage.input_tokens,
    )

    print(
        "Output tokens:",
        result.usage.output_tokens,
    )

    print(
        "Total tokens:",
        result.usage.total_tokens,
    )

    print(
        "Context tokens:",
        result.usage.context_tokens,
    )

    print(
        "Utilization:",
        result.usage.utilization,
    )

    print(
        "Estimated:",
        result.usage.estimated,
    )


# -------------------------
# Thinking
# -------------------------

print("\nTHINKING LEVEL")
print(result.thinking_level)


# -------------------------
# Sources
# -------------------------

print("\nSOURCES")

for source in result.sources:
    print(source)


# -------------------------
# Evaluation
# -------------------------

evaluation = rag.evaluate([
    {
        "question": (
            "Explain our production deployment process."
        ),

        "expected_answer": (
            "Production deployment is approved "
            "and executed through the defined "
            "deployment process."
        ),
    }
])


print("\nEVALUATION")

print(
    evaluation["averages"]
)
```

Run:

```bash
python app.py
```

---

# 43. Minimal Application

If you don't need advanced features, you only need:

```python
from prashflow import RAG

rag = RAG(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },
)

print(
    rag.ask("What is PrashFlow?")
)
```

You can start here and add advanced features later.

---

# 44. Feature Summary

After installing:

```bash
pip install prashflow
```

you can build:

```text
PrashFlow
|
+-- RAG
|   +-- Documents
|   +-- Embeddings
|   +-- Vector DB
|   +-- Retrieval
|   +-- MMR
|   +-- Reranking
|   +-- Context
|   +-- Prompts
|   +-- Sessions
|
+-- LLM
|   +-- Ollama
|   +-- OpenAI
|   +-- LiteLLM
|   +-- Model Routing
|   +-- Automatic Fallback
|
+-- Runtime
|   +-- Context Window
|   +-- Token Usage
|   +-- Token Utilization
|   +-- Thinking Levels
|   +-- Health Checks
|
+-- Evaluation
|   +-- Context Relevance
|   +-- Context Recall
|   +-- Answer Relevance
|   +-- Faithfulness
|   +-- Source Recall
|   +-- Overall Score
|
+-- Agents
|   +-- Agent
|   +-- AgentChat
|   +-- Multi-Agent
|   +-- Tools
|
+-- Protocols
    +-- MCP
    +-- A2A
        +-- Client
        +-- Registry
        +-- Router
        +-- Orchestrator
        +-- Server
```

---

# 45. Recommended Learning Path

If you are new to PrashFlow:

### Step 1 — Basic RAG

```python
rag = RAG(...)
rag.ask(...)
```

### Step 2 — Add Params

```python
Params(
    top_k=5,
    thinking_level="medium",
)
```

### Step 3 — Add usage tracking

```python
rag.ask_result(...)
```

### Step 4 — Add model fallback

```python
llm={
    "strategy": "priority",
    "models": [...]
}
```

### Step 5 — Add health checks

```python
rag.health_check()
```

### Step 6 — Evaluate RAG

```python
rag.evaluate([...])
```

### Step 7 — Build agents

```python
Agent(...)
```

### Step 8 — Add MCP

Connect external tools.

### Step 9 — Add A2A

Connect remote agents.

### Step 10 — Build your production AI application

```text
RAG
+
Agents
+
Tools
+
MCP
+
A2A
+
Model Routing
+
Fallback
+
Usage
+
Evaluation
+
Health
```

---

# 46. Important End-User Rule

You normally **do not need to import or modify PrashFlow's internal modules**.

Prefer:

```python
from prashflow import RAG, Params
```

instead of importing internal implementation files.

Your application should look like:

```text
my-ai-app/
|
+-- app.py
+-- documents/
+-- chroma_db/
+-- .env
└-- requirements.txt
```

and PrashFlow should be installed as:

```bash
pip install prashflow
```

The framework handles the underlying orchestration.

---

# 47. Final Quick Start

Install:

```bash
pip install prashflow
```

Install Ollama models if using Ollama:

```bash
ollama pull qwen3:8b
ollama pull nomic-embed-text
```

Create:

```python
from prashflow import RAG, Params

rag = RAG(
    llm={
        "provider": "ollama",
        "model": "qwen3:8b",
    },

    embeddings={
        "provider": "ollama",
        "model": "nomic-embed-text",
    },

    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
    },

    params=Params(
        thinking_level="medium",
        top_k=8,
        track_usage=True,
    ),
)

result = rag.ask_result(
    "What is PrashFlow?"
)

print(result.answer)

if result.usage:
    print(
        "Total tokens:",
        result.usage.total_tokens,
    )
```
```python
"""
PrashFlow 1.2 - Production-style Client RAG Application

Features:
    - Document ingestion
    - Persistent Chroma vector DB
    - Hybrid semantic + BM25 retrieval
    - Optional reranking
    - Model routing / fallback
    - Streaming RAG chat
    - Session memory
    - Source display
    - Usage statistics
    - Health checks
    - RAG evaluation
    - Interactive CLI

Install:
    pip install "prashflow[all]"

For Ollama:
    ollama pull qwen3:8b
    ollama pull nomic-embed-text

Run:
    python app.py
"""

from pathlib import Path

from prashflow import RAG, Params


# ============================================================
# APPLICATION CONFIGURATION
# ============================================================

APP_NAME = "PrashFlow Enterprise RAG"

KNOWLEDGE_DIR = "./knowledge"
VECTOR_DIR = "./data/chroma"

COLLECTION_NAME = "company_knowledge"

SESSION_ID = "user-001"


# ============================================================
# MODEL CONFIGURATION
# ============================================================

# Primary + fallback models.
#
# If you only have one model, simply keep one entry.
#
# PrashFlow will try the first model and fall back to
# the next model if the first one fails.

LLM_CONFIG = {
    "strategy": "priority",

    "models": [
        {
            "provider": "ollama",
            "model": "qwen3:8b",
            "base_url": "http://localhost:11434",
        },

        {
            "provider": "ollama",
            "model": "llama3.1:8b",
            "base_url": "http://localhost:11434",
        },
    ],

    "fallback_on_error": True,
}


# Embedding model.
#
# Keep embeddings stable once your knowledge base is indexed.
EMBEDDING_CONFIG = {
    "provider": "ollama",
    "model": "nomic-embed-text",
    "base_url": "http://localhost:11434",
}


# ============================================================
# PRASHFLOW PARAMETERS
# ============================================================

PARAMS = Params(
    # Model/context controls
    context_window=32768,

    # Don't send unnecessarily huge context to the LLM.
    max_context_tokens=6000,

    # Reserve output space.
    reserve_output_tokens=1200,

    # Retrieval defaults
    top_k=4,
    search_type="hybrid",

    # Usage tracking
    track_usage=True,

    # Resilience
    fallback_on_error=True,

    # IMPORTANT:
    # Don't perform a health request before EVERY query.
    # That adds latency.
    health_check_before_request=False,

    health_check_timeout=5.0,

    # Evaluation disabled during normal user requests.
    evaluation_enabled=False,
)


# ============================================================
# RAG ENGINE
# ============================================================

rag = RAG(

    # --------------------------------------------------------
    # LLM / MODEL ROUTING
    # --------------------------------------------------------

    llm=LLM_CONFIG,

    # --------------------------------------------------------
    # EMBEDDINGS
    # --------------------------------------------------------

    embeddings=EMBEDDING_CONFIG,

    # --------------------------------------------------------
    # VECTOR DATABASE
    # --------------------------------------------------------

    vector_db={
        "provider": "chroma",

        "path": VECTOR_DIR,

        "collection": COLLECTION_NAME,
    },

    # --------------------------------------------------------
    # RETRIEVAL
    # --------------------------------------------------------

    retrieval={
        # Best general-purpose default.
        "type": "hybrid",

        # Final documents sent to the LLM.
        "top_k": 4,

        # Candidates retrieved before fusion.
        #
        # Keep this reasonably small for latency.
        "candidate_k": 12,

        # Hybrid weighting.
        "semantic_weight": 0.65,
        "keyword_weight": 0.35,
    },

    # --------------------------------------------------------
    # RERANKER
    # --------------------------------------------------------

    # Disabled for maximum speed.
    #
    # Enable when retrieval precision is more important
    # than latency.
    reranker={
        "enabled": False,
    },

    # --------------------------------------------------------
    # CHUNKING
    # --------------------------------------------------------

    chunking={
        "size": 900,
        "overlap": 120,
    },

    # --------------------------------------------------------
    # PRASHFLOW PARAMS
    # --------------------------------------------------------

    params=PARAMS,

    # Default session.
    session_id=SESSION_ID,
)


# ============================================================
# INGESTION
# ============================================================

def ingest_knowledge():
    """
    Index all documents under ./knowledge.

    Example:

        knowledge/
        ├── architecture.pdf
        ├── deployment.docx
        ├── security.md
        ├── troubleshooting.txt
        └── faq.csv
    """

    knowledge = Path(KNOWLEDGE_DIR)

    if not knowledge.exists():
        knowledge.mkdir(
            parents=True,
            exist_ok=True,
        )

        print(
            f"\nCreated knowledge directory: {knowledge}"
        )

        print(
            "Put your documents inside it and run again."
        )

        return

    supported = (
        ".pdf",
        ".docx",
        ".txt",
        ".md",
        ".csv",
    )

    files = [
        file
        for file in knowledge.rglob("*")
        if file.is_file()
        and file.suffix.lower() in supported
    ]

    if not files:
        print("\nNo supported documents found.")

        print(
            "Supported: PDF, DOCX, TXT, MD, CSV"
        )

        return

    print(
        f"\nFound {len(files)} documents."
    )

    print("Starting ingestion...\n")

    # PrashFlow handles loading, chunking,
    # embeddings and vector storage.
    #
    # Calling ingest on the directory keeps
    # application code extremely small.
    rag.ingest(KNOWLEDGE_DIR)

    print("\nKnowledge base ready.")


# ============================================================
# HEALTH CHECK
# ============================================================

def health_check():
    """
    Run a deep PrashFlow health check.

    This checks:
        - LLM
        - embeddings
        - vector DB
    """

    print("\nRunning PrashFlow health check...\n")

    result = rag.health_check(
        deep=True
    )

    print(
        f"Overall: "
        f"{'HEALTHY' if result['healthy'] else 'UNHEALTHY'}"
    )

    print(
        f"LLM: {result.get('llm')}"
    )

    print(
        f"Embeddings: {result.get('embeddings')}"
    )

    print(
        f"Vector DB: {result.get('vector_db')}"
    )

    return result


# ============================================================
# ONE-SHOT RAG
# ============================================================

def ask_once(question: str):
    """
    Non-streaming RAG.

    Useful for scripts, APIs and automation.
    """

    result = rag.ask_result(
        question,

        search_type="hybrid",

        top_k=4,

        candidate_k=12,

        # Keep reranking disabled for speed.
        rerank=False,
    )

    print("\n" + "=" * 70)

    print("ANSWER")

    print("=" * 70)

    print(result.answer)

    print("\n" + "=" * 70)

    print("SOURCES")

    print("=" * 70)

    for source in result.sources:

        print(
            f"- {source.get('source')}"
        )

    if result.usage:

        print("\n" + "=" * 70)

        print("USAGE")

        print("=" * 70)

        print(
            f"Input tokens : "
            f"{result.usage.input_tokens}"
        )

        print(
            f"Output tokens: "
            f"{result.usage.output_tokens}"
        )

        print(
            f"Total tokens : "
            f"{result.usage.total_tokens}"
        )

    return result


# ============================================================
# STREAMING RAG CHAT
# ============================================================

def chat():
    """
    Interactive conversational RAG.

    Uses PrashFlow chat_stream().

    Conversation history is maintained using
    the PrashFlow session.
    """

    print("\n" + "=" * 70)

    print(APP_NAME)

    print("=" * 70)

    print(
        "\nType your question."
    )

    print(
        "Commands:"
    )

    print(
        "  /exit     Exit"
    )

    print(
        "  /health   Health check"
    )

    print(
        "  /clear    Clear conversation"
    )

    print(
        "  /search   Change retrieval mode"
    )

    print()

    search_type = "hybrid"

    while True:

        try:

            question = input("You: ").strip()

        except (
            KeyboardInterrupt,
            EOFError,
        ):

            print("\nGoodbye.")

            break

        if not question:
            continue

        # ----------------------------------------------------
        # EXIT
        # ----------------------------------------------------

        if question.lower() in {
            "/exit",
            "exit",
            "quit",
        }:

            print("Goodbye.")

            break

        # ----------------------------------------------------
        # HEALTH
        # ----------------------------------------------------

        if question.lower() == "/health":

            health_check()

            continue

        # ----------------------------------------------------
        # CLEAR SESSION
        # ----------------------------------------------------

        if question.lower() == "/clear":

            rag.clear_session(
                SESSION_ID
            )

            print(
                "Conversation cleared."
            )

            continue

        # ----------------------------------------------------
        # SEARCH MODE
        # ----------------------------------------------------

        if question.lower() == "/search":

            print(
                "\nChoose search type:"
            )

            print(
                "1. hybrid"
            )

            print(
                "2. semantic"
            )

            print(
                "3. keyword"
            )

            print(
                "4. mmr"
            )

            print(
                "5. multi_query"
            )

            choice = input(
                "\nChoice: "
            ).strip()

            modes = {
                "1": "hybrid",
                "2": "semantic",
                "3": "keyword",
                "4": "mmr",
                "5": "multi_query",
            }

            search_type = modes.get(
                choice,
                "hybrid",
            )

            print(
                f"Search mode: {search_type}"
            )

            continue

        # ----------------------------------------------------
        # STREAMING RAG
        # ----------------------------------------------------

        print("\nAI: ", end="", flush=True)

        try:

            for token in rag.chat_stream(

                query=question,

                session_id=SESSION_ID,

                search_type=search_type,

                top_k=4,

                candidate_k=12,

                # Maximum speed.
                rerank=False,
            ):

                print(
                    token,
                    end="",
                    flush=True,
                )

            print("\n")

            # ------------------------------------------------
            # USAGE
            # ------------------------------------------------

            if rag.last_usage:

                usage = rag.last_usage

                print(
                    f"[tokens: "
                    f"{usage.total_tokens}]"
                )

                print()

        except Exception as exc:

            print(
                f"\nRAG error: {exc}\n"
            )


# ============================================================
# EVALUATION
# ============================================================

def evaluate():

    """
    Run a small RAG evaluation set.

    This should normally be used during development/CI,
    not on every user request.
    """

    test_cases = [

        {
            "question":
                "What is the production deployment process?",

            "expected_answer":
                "Production deployment requires approval "
                "and validation.",

            "expected_sources": [
                "deployment.pdf"
            ],
        },

        {
            "question":
                "How do I troubleshoot a failed deployment?",

            "expected_answer":
                "Check the deployment logs and "
                "validate the failed stage.",
        },
    ]

    print(
        "\nRunning RAG evaluation...\n"
    )

    result = rag.evaluate(
        test_cases
    )

    print(result)

    return result


# ============================================================
# APPLICATION MENU
# ============================================================

def main():

    print("\n" + "=" * 70)

    print(APP_NAME)

    print("=" * 70)

    print(
        "\n1. Ingest knowledge"
    )

    print(
        "2. Health check"
    )

    print(
        "3. Chat"
    )

    print(
        "4. Ask one question"
    )

    print(
        "5. Evaluate RAG"
    )

    print(
        "6. Exit"
    )

    while True:

        choice = input(
            "\nSelect: "
        ).strip()

        if choice == "1":

            ingest_knowledge()

        elif choice == "2":

            health_check()

        elif choice == "3":

            chat()

        elif choice == "4":

            question = input(
                "\nQuestion: "
            ).strip()

            if question:

                ask_once(question)

        elif choice == "5":

            evaluate()

        elif choice == "6":

            print(
                "Goodbye."
            )

            break

        else:

            print(
                "Invalid option."
            )


# ============================================================
# ENTRY POINT
# ============================================================

if __name__ == "__main__":

    main()


```


That's all you need to get started.

**Install PrashFlow → configure your LLM → configure embeddings → configure vector DB → build your AI application.**


## PrashFlow 1.3 — Guardrails, Human-in-the-Loop & Incremental RAG

PrashFlow 1.3 keeps the existing v1.2 APIs and file layout intact while adding
opt-in production safety and efficient RAG synchronization.

### Model-agnostic guardrails

Guardrails can use deterministic rules, custom Python functions, a dedicated
security/classifier model, an LLM judge, or an external HTTP guardrail service.
The guard model is independent of the application's generation model.

```python
from prashflow import RAG

rag = RAG(
    llm={"provider": "ollama", "model": "qwen3:8b"},
    embeddings={"provider": "ollama", "model": "nomic-embed-text"},
    vector_db={
        "provider": "chroma",
        "path": "./chroma_db",
        "collection": "company_docs",
    },
    guardrails={
        "enabled": True,
        "prompt_injection": True,
        "pii": {"action": "redact"},
        "input": {"max_length": 8000},
        "grounding": {"action": "warn", "threshold": 0.15},
    },
)
```

Supported guard decisions:

`allow`, `deny`, `block`, `warn`, `redact`, `retry`, `approval`.

A model-backed guard can be configured independently:

```python
guardrails={
    "enabled": True,
    "prompt_injection": {
        "model": {
            "provider": "ollama",
            "model": "llama-guard",
        }
    },
}
```

Custom guards are also supported:

```python
def my_policy(stage, value, metadata):
    if stage == "tool" and metadata.get("tool") == "delete_production":
        return {"decision": "deny", "risk": "critical"}
    return {"decision": "allow"}

guardrails = {
    "enabled": True,
    "custom": [my_policy],
}
```

### Tool security

```python
from prashflow import Agent

agent = Agent(
    llm="ollama:qwen3:8b",
    tools=[database_tool, deploy_tool],
    guardrails={
        "enabled": True,
        "tools": {
            "database_tool": {"risk": "low", "action": "allow"},
            "deploy_tool": {"risk": "high", "action": "approval"},
            "delete_database": {"risk": "critical", "action": "deny"},
        },
    },
    human_in_loop={
        "enabled": True,
        "provider": "cli",
    },
)
```

### Human-in-the-loop

The same approval engine supports four interfaces:

- `cli` — local/development applications
- `callback` — custom Python applications
- `api` — web/mobile/custom UI
- `streamlit` — quick internal AI applications

CLI:

```python
human_in_loop={"enabled": True, "provider": "cli"}
```

Callback:

```python
def approve(request):
    return input(f"Approve {request.action}? [y/N] ").lower() == "y"

human_in_loop={
    "enabled": True,
    "provider": "callback",
    "callback": approve,
}
```

REST API:

```python
from prashflow import ApprovalManager

manager = ApprovalManager(
    provider="api",
    auto_start_api=True,
    api_host="127.0.0.1",
    api_port=8765,
)
```

Endpoints:

```text
GET  /approvals
GET  /approvals/{approval_id}
POST /approvals/{approval_id}/approve
POST /approvals/{approval_id}/reject
```

Install REST/Streamlit support with:

```bash
pip install "prashflow[hitl]"
```

Streamlit:

```python
from prashflow import ApprovalManager

manager = ApprovalManager(provider="streamlit")
manager.streamlit()
```

The four frontends share the same `ApprovalRequest` and approval state.
Agents pause while an API/Streamlit approval is pending and resume after
approval or terminate after rejection/expiry.

### Incremental RAG

Enable incremental indexing without changing the existing `rag.ingest()`
behavior:

```python
rag = RAG(
    llm={...},
    embeddings={...},
    vector_db={...},
    incremental={
        "enabled": True,
        "manifest": "./.prashflow/index_manifest.json",
        "bm25": "./.prashflow/bm25.json",
    },
)

result = rag.sync("./docs")
print(result)
```

Example result:

```text
{
    "scanned": 1250,
    "new": 12,
    "modified": 7,
    "deleted": 3,
    "unchanged": 1228,
    "chunks_indexed": 184,
    "chunks_removed": 41
}
```

Only new/modified documents are embedded. Deleted documents have their old
vectors removed. The BM25 sidecar is persisted so hybrid retrieval survives a
process restart when incremental mode is enabled.

The existing:

```python
rag.ingest("./docs")
```

remains unchanged.

### Important compatibility rule

All v1.3 capabilities are opt-in. Existing v1.0/v1.2 code does not need to
be rewritten and existing RAG, Agent, MCP, A2A, routing, fallback, streaming,
evaluation and vector-store APIs remain available through:

```python
from prashflow import RAG, Agent, Params
```
