Agently Docs

Agently documentation for building AI applications with stable outputs, observable actions, and durable workflows.

View the Project on GitHub AgentEra/Agently

Knowledge Base Dialog

Languages: English · 中文

The problem

A user asks questions in natural language about a document collection (product docs, policies, internal wiki). Each turn:

  1. Retrieve the relevant passages.
  2. Answer using only those passages.
  3. Cite sources.
  4. Maintain conversation context across turns.

The shape

User question
   │
   ▼
Retrieve top-K from KB        ◄── via embedding agent + vector store
   │
   ▼
Agent.input(question).info({"sources": chunks}, always=False).output({...})
   │
   ▼
Structured answer + cited sources
   │
   ▼
Append to session history

Walkthrough

from agently import Agently

# Embedding agent — small / fast model, just for vectorization
embedding_agent = Agently.create_agent().set_settings("OpenAICompatible", {
    "base_url": "...",
    "api_key": "...",
    "model": "${ENV.EMBEDDING_MODEL}",
})

# Answering agent — reasoning model
agent = (
    Agently.create_agent()
    .role(
        "Answer using ONLY the provided sources. If the sources do not "
        "contain the answer, say so explicitly.",
        always=True,
    )
)
agent.activate_session(session_id="kb-dialog")  # multi-turn

from agently.integrations.chromadb import ChromaCollection
collection = ChromaCollection(collection_name="docs", embedding_agent=embedding_agent)


def ask(user_question: str):
    chunks = collection.query(user_question, top_n=5)
    return (
        agent
        .info({"sources": chunks}, always=False)
        .input(user_question)
        .output({
            "answer": (str, "Direct answer", True),
            "citations": [
                {
                    "source_id": (str, "Source id from the provided sources", True),
                    "quote": (str, "Short verbatim quote", True),
                }
            ],
            "uncertain": (bool, "True if the sources do not fully answer the question", True),
        })
        .start()
    )


# Loop
while True:
    user_text = input("> ")
    if not user_text.strip():
        break
    result = ask(user_text)
    print(result["answer"])
    for c in result["citations"]:
        print(f"  [{c['source_id']}] {c['quote']}")
    if result["uncertain"]:
        print("  (the sources do not fully cover this question)")

Why these choices

Variations

Filter retrieval per user

If your KB has multiple tenants or users, scope retrieval at query time:

chunks = collection.query(user_question, top_n=5, where={"tenant_id": current_user.tenant_id})

Validate citations against the retrieved set

Add .validate(...) to make sure cited source_ids actually appear in the retrieved chunks:

def cite_check(result, ctx):
    valid_ids = {c["id"] for c in ctx.input.get("sources", [])}
    bad = [c for c in result["citations"] if c["source_id"] not in valid_ids]
    if bad:
        return {"ok": False, "reason": "fabricated source_id", "validator_name": "citation"}
    return True

See Output Control.

Stream the answer

If the user is reading interactively, stream the answer field:

gen = agent.info({"sources": chunks}, always=False).input(user_text).output({...}).get_generator(type="instant")
for item in gen:
    if item.path == "answer" and item.delta:
        print(item.delta, end="", flush=True)