Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

difficult_dialogs

difficult_dialogs is a structured argumentation framework. An LLM generates the content of a debate once, as plain-text argument files. A deterministic policy engine then runs the debate any number of times, without another LLM call.

Tests Coverage License Python 3.10+


Why use it

An LLM used directly for a debate has four problems. It can hallucinate facts mid-conversation. It costs money on every call at scale. It adds two to five seconds of latency per turn. It needs network access.

difficult_dialogs separates the content of an argument from its delivery. Generate the argument once with an LLM, about sixty seconds per topic. Store it as plain-text files: .premise, .support, .source, .why, .who, and related fields. From then on, a policy engine runs the debate in under one millisecond per turn, offline, at no per-turn cost. The files are auditable and work with git diff.

You can also layer an LLMEnhancedPolicy on top of any policy to rephrase responses at runtime, without changing the underlying argument structure.


Install

uv pip install difficult-dialogs                  # core + OVOS yes/no solver
uv pip install "difficult-dialogs[server]"        # + FastAPI REST server
uv pip install "difficult-dialogs[dev]"           # + pytest, ruff, mypy

Python 3.10+. Requires ovos-plugin-manager, ovos-solver-yes-no-plugin, ovos-solver-bm25-plugin, and rank-bm25 (all installed automatically).


Quick start

CLI

did debate    examples/i_think_therefore_i_am        # interactive debate
did list      examples/sample_arguments              # browse library
did validate  examples/sample_arguments              # quality report
did serve     --port 8080                            # REST API server
did generate  "Solar energy is cost-effective" \
             --server http://localhost:8000          # LLM-generate an argument

Python

from difficult_dialogs import Argument, KnowItAllPolicy

arg = Argument.from_directory("examples/i_think_therefore_i_am")
policy = KnowItAllPolicy(arg)

print(policy.start())

gen = policy.run_sync()
response = next(gen)
while response:
    print("BOT:", response)
    try:
        response = gen.send(input("USER: "))
    except StopIteration:
        break

Fluent builder

from difficult_dialogs.builder import ArgumentBuilder

arg = (
    ArgumentBuilder("climate_change")
    .intro("Let's discuss climate change.")
    .conclusion("The evidence is clear.")
    .premise("human_causation")
        .statement("97% of climate scientists agree.")
        .support("See IPCC AR6.")
        .source("https://www.ipcc.ch/")
        .why("CO₂ traps heat in the atmosphere.")
        .who("Climate scientists and IPCC working groups.")
        .done()
    .build()
)

File format

Arguments are plain-text directories, one subdirectory per premise:

argument_name/
├── intro.dialog              # Opening statement
├── conclusion.conclusion     # Closing statement
└── premise_name/
    ├── premise_name.premise  # Core claims (one per line)
    ├── premise_name.support  # Comeback arguments (optional)
    ├── premise_name.source   # Citation URLs (optional)
    ├── premise_name.what     # Six-Ws contextual fields (optional)
    ├── premise_name.why
    ├── premise_name.how
    ├── premise_name.when
    ├── premise_name.where
    └── premise_name.who

UTF-8, one entry per line. Works with git diff, grep, any text editor. Full reference: docs/argument-format.md


Graph visualization

Arguments with branching (.on_agree/.on_disagree/.choices) form directed graphs. View a graph with did graph:

did graph examples/sample_arguments/technology/should_ai_be_regulated
graph TD
    ai_risk(["ai_risk\n(2 stmts)"])
    oversight{"oversight\n(2 stmts)"}
    innovation["innovation\n(2 stmts)"]
    balance["balance\n(2 stmts)"]
    implementation["implementation\n(2 stmts)"]
    ai_risk -->|"agree"| oversight
    ai_risk -->|"disagree"| innovation
    oversight -->|"choice:A"| implementation
    oversight -->|"choice:B"| balance
    innovation -->|"agree"| balance
    innovation -->|"disagree"| oversight
    style ai_risk stroke-width:3px

Also available as DOT (--format dot) and JSON (--format json). Browse all sample graphs: docs/GRAPHS.md


Policies

A policy sets the "personality" of the dialog. The argument content stays the same across all policies.

Policy Style Uses support Asks questions
KnowItAllPolicy Persuasive: corrects with evidence Yes No
SilentPolicy Lecture: no interaction No No
SocraticPolicy Questioning, using the Socratic method Yes Yes
DebatePolicy Adversarial: challenges the user Yes Yes
ExploratoryPolicy Neutral: presents multiple sides Yes Yes
MaieuticPolicy Guided discovery Yes Yes
SkepticPolicy Doubting: the user must prove the point Yes Yes
TeacherPolicy Educational, with a lesson structure Yes Yes
DebaterPolicy Formal debate rules Yes Yes
MinimalistPolicy Terse confirmations only No No
AdaptivePolicy Switches policy after N disagreements via inner via inner
WebhookPolicy Forwards to an HTTP endpoint, with a local fallback Yes No
LLMEnhancedPolicy Rephrases responses through an LLM at runtime via inner via inner
MultiArgumentPolicy Chains multiple arguments in sequence via inner via inner
MultiChoicePolicy Presents labeled choices (A/B/C) each turn Yes Yes
CooperativePolicy Acknowledges disagreement and seeks common ground Yes Yes
from difficult_dialogs import get_policy, Argument

arg = Argument.from_directory("my_argument")
policy = get_policy("socratic", arg)   # by name via POLICY_REGISTRY

Full reference: docs/POLICIES.md

LLMEnhancedPolicy

Add natural language variety to any policy without changing its argument logic:

from difficult_dialogs.policy import LLMEnhancedPolicy, KnowItAllPolicy
from difficult_dialogs.llm import LLMEnhancer

enhancer = LLMEnhancer("http://localhost:8000", model="qwen-7b")
policy = LLMEnhancedPolicy(
    arg,
    inner_policy=KnowItAllPolicy(arg),
    enhancer=enhancer,
    style="friendly",   # "conversational" | "formal" | "friendly" | "academic"
)

If the LLM server is unreachable, the policy falls back to the original text.

AdaptivePolicy

Softens its approach automatically when the user keeps disagreeing:

from difficult_dialogs import AdaptivePolicy, KnowItAllPolicy, ExploratoryPolicy

policy = AdaptivePolicy(
    arg,
    initial_policy=KnowItAllPolicy(arg),
    fallback_policy=ExploratoryPolicy(arg),
    switch_threshold=3,
)

MultiArgumentPolicy

Chain multiple arguments into one session:

from difficult_dialogs import MultiArgumentPolicy

policy = MultiArgumentPolicy(
    [(intro_arg, "silent"), (main_arg, "knowitall"), (close_arg, "minimalist")]
)

Session state & transcripts

# Save
state_dict = policy.state.to_dict()   # JSON-safe dict

# Restore
policy.restore_state(state_dict)

# Export transcript
from difficult_dialogs.export.transcript import export_transcript_to_markdown
md = export_transcript_to_markdown(policy, title="Session 1")

from difficult_dialogs.library import ArgumentLibrary

lib = ArgumentLibrary("arguments/").scan()
results = lib.search("climate change", limit=5)
for r in results:
    print(r.argument.name, r.score)

lib.by_category("health")    # all arguments in a top-level subdirectory
lib.get("free_will_exists")  # direct lookup by name

Export

from difficult_dialogs.export import export_to_json, export_to_markdown
from difficult_dialogs.export.sqlite import LibraryDatabase

export_to_json(arg, "argument.json")
export_to_markdown(arg, "argument.md")

db = LibraryDatabase("library.db")
db.add_argument(arg, category="philosophy")
db.close()

# Argument diff — review LLM-generated changes before committing
diff = original.diff(updated)
# {"meta": ..., "added_premises": [...], "removed_premises": [...], "modified_premises": {...}}

LLM generation

Requires an OpenAI-compatible server (Ollama, llama.cpp, OpenAI API, etc.):

from difficult_dialogs.llm import ArgumentGenerator

gen = ArgumentGenerator("http://localhost:8000", model="qwen-72b")
arg = gen.generate("Solar energy is cost-effective", stance="pro", depth=2)
arg.save("arguments/solar_energy")

Or from the CLI:

did generate "Solar energy is cost-effective" \
  --server http://localhost:11434 \
  --model qwen-72b \
  --output arguments/

Supported servers:

ollama serve                                               # http://localhost:11434
./llama-server -m model.gguf --host 0.0.0.0 --port 8000   # http://localhost:8000
# OpenAI: --server https://api.openai.com/v1 --model gpt-4o

REST server

did serve --host 0.0.0.0 --port 8080
Endpoint Description
POST /sessions Create session (argument_path, policy)
POST /sessions/{id}/chat Send turn (user_input)
GET /sessions/{id} Full session info
GET /sessions/{id}/state Serialized PolicyState
PUT /sessions/{id}/state Restore saved state
DELETE /sessions/{id} End session
GET /arguments List available argument directories

Validation

from difficult_dialogs.validators import validate_argument, ArgumentValidator

result = validate_argument(arg)
print(result.score)                                  # 0.0-1.0
print(ArgumentValidator().get_quality_label(result.score))  # "Excellent ⭐" | "Good 👍" | "Fair 😐" | "Poor ❌"
for issue in result.issues:
    print(issue)

Testing

uv run pytest test/ -v
uv run pytest test/ --cov=difficult_dialogs --cov-report=term-missing

701 tests, 97% average coverage.


Project layout

difficult_dialogs/
├── statements.py        # Statement dataclass
├── premises.py          # Premise — Six Ws: what/why/how/when/where/who
├── arguments.py         # Argument — load/save/diff/merge
├── builder.py           # ArgumentBuilder / PremiseBuilder fluent API
├── policy.py            # BasePolicy + 16 concrete policies + registry
├── library.py           # ArgumentLibrary — keyword search
├── validators.py        # Quality scoring
├── server.py            # FastAPI REST server
├── cli.py               # did / difficult-dialogs CLI
├── export/
│   ├── json.py          # JSON export
│   ├── sqlite.py        # SQLite export
│   ├── markdown.py      # Markdown export
│   └── transcript.py    # Session transcript export
└── llm/
    ├── client.py        # OpenAI-compatible HTTP client
    ├── generator.py     # Argument generation from topic string
    └── enhancer.py      # Runtime statement rephrasing

Comparison

difficult_dialogs Raw LLM Other debate tools
Cost per debate $0 after generation $0.10-0.50 Varies
Latency Under 1 ms 2-5 s 1-3 s
Offline Yes No No
Hallucinations None Possible Sometimes
Auditable content Yes, plain text No, black box Limited
Git-versionable Yes No Partial
LLM enhancement Optional Required N/A

Documentation


Contributing

git clone https://github.com/TigreGotico/difficult_dialogs
cd difficult_dialogs
uv pip install -e ".[dev]"
uv run pytest test/ -v

PRs target the dev branch. Report issues on the issue tracker.



License

Apache 2.0. See LICENSE.

Metadata

Release files for difficult-dialogs 0.6.1a2

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

Source distribution (sdist)

Source distribution for difficult-dialogs 0.6.1a2
File Size Uploaded
difficult_dialogs-0.6.1a2.tar.gz 137.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for difficult-dialogs 0.6.1a2
File Interpreter ABI Platform
difficult_dialogs-0.6.1a2-py3-none-any.whl Python 3 none any Details

Total release size: 221.8 kB

Release files / difficult_dialogs-0.6.1a2.tar.gz

Download URL difficult_dialogs-0.6.1a2.tar.gz
Size 137.4 kB
Tags Source
SHA-256 checksum
How to use checksums
17c7aa498fbe5ae1e08712ff524bf1037ef5c21eb048b2aef8ffe17d58a9f173
BLAKE2b-256 checksum
How to use checksums
e9b038cd18a7f22b865356140f4a4189416d070a975b9f6538da330e11ade76a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / difficult_dialogs-0.6.1a2-py3-none-any.whl

Download URL difficult_dialogs-0.6.1a2-py3-none-any.whl
Size 84.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3947957a6ff83f3facf941b33c382b60701269d6051bbfd5c3016f18951d963f
BLAKE2b-256 checksum
How to use checksums
1f6954b28354d8d3cd1b18399d6b19401cafa56277ac3a53168494edfb6101ff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page