Skip to main content

Structured argument framework for meaningful debates

Project description

difficult_dialogs

Structured argumentation framework — compile knowledge once, run debates forever.

Tests Coverage License Python 3.10+


The Problem

LLMs are great for debates, but they:

  • ❌ Hallucinate facts mid-conversation
  • ❌ Cost money per call at scale
  • ❌ Add 2–5 s latency per turn
  • ❌ Require network access

The Solution

Generate arguments once with an LLM. Run them forever without one.

┌──────────────────┐     ┌──────────────────────┐     ┌───────────────────┐
│  LLM (one-time)  │     │  Plain-text files     │     │  Policy engine    │
│  generate once   │────▶│  .premise  .support   │────▶│  deterministic    │
│  ~60 s / topic   │     │  .source   .why  .who │     │  <1 ms / turn     │
└──────────────────┘     └──────────────────────┘     └───────────────────┘
      Smart                  Auditable, git-friendly       Offline, free

Optionally layer an LLMEnhancedPolicy on top to rephrase responses at runtime — without changing the locked 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

dd debate    examples/i_think_therefore_i_am        # interactive debate
dd list      examples/sample_arguments              # browse library
dd validate  examples/sample_arguments              # quality report
dd serve     --port 8080                            # REST API server
dd 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. Visualize 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

Policies are the "personality" of the dialog. The argument content never changes.

Policy Style Uses support Asks questions
KnowItAllPolicy Persuasive — corrects with evidence
SilentPolicy Lecture — no interaction
SocraticPolicy Questioning — Socratic method
DebatePolicy Adversarial — challenges user
ExploratoryPolicy Neutral — presents multiple sides
MaieuticPolicy Guided discovery
SkepticPolicy Doubting — user must prove it
TeacherPolicy Educational — lesson structure
DebaterPolicy Formal debate rules
MinimalistPolicy Terse confirmations only
AdaptivePolicy Switches policy after N disagreements via inner via inner
WebhookPolicy Forwards to HTTP endpoint, local fallback
LLMEnhancedPolicy Rephrases responses via LLM at runtime via inner via inner
MultiArgumentPolicy Chains multiple arguments sequentially via inner via inner
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"
)

Falls back silently to original text if the LLM server is unreachable.

AdaptivePolicy

Automatically softens approach 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")

Argument library search

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:

dd 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

dd 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, get_quality_label

result = validate_argument(arg)
print(result.score)                     # 0.0–1.0
print(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

794 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 + 14 concrete policies + registry
├── library.py           # ArgumentLibrary — keyword search
├── validators.py        # Quality scoring
├── server.py            # FastAPI REST server
├── cli.py               # dd / 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 <1 ms 2–5 s 1–3 s
Offline
Hallucinations ❌ none ✅ possible ⚠️ sometimes
Auditable content ✅ plain text ❌ black box ⚠️ limited
Git-versionable ⚠️
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. Issues: https://github.com/TigreGotico/difficult_dialogs/issues


License

Apache 2.0 — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

difficult_dialogs-0.6.0a1.tar.gz (137.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

difficult_dialogs-0.6.0a1-py3-none-any.whl (84.2 kB view details)

Uploaded Python 3

File details

Details for the file difficult_dialogs-0.6.0a1.tar.gz.

File metadata

  • Download URL: difficult_dialogs-0.6.0a1.tar.gz
  • Upload date:
  • Size: 137.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for difficult_dialogs-0.6.0a1.tar.gz
Algorithm Hash digest
SHA256 0c550f9e40058f051966a909f7ead47e6d00c8a678d9b48ac948be8799ab9049
MD5 02305f639ad912916dbd84e89a4c37ff
BLAKE2b-256 4d39578738b66c735ea1b80874741ff9e08965b4cdae816250d49431a960bdac

See more details on using hashes here.

File details

Details for the file difficult_dialogs-0.6.0a1-py3-none-any.whl.

File metadata

File hashes

Hashes for difficult_dialogs-0.6.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 3b7c187598f723b3da05589de96eabb88d667cb57d8c4195b66a6de7ee622fe9
MD5 7e91676df130cd43073e3c44b3936bc5
BLAKE2b-256 319149e04d9b80d324f20f89096544322d930963a461c7b6d12a2a7d6999456b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page