{"id":50339666,"url":"https://github.com/golikovichev/pytest-conversational","last_synced_at":"2026-06-15T07:01:42.308Z","repository":{"id":355196385,"uuid":"1225547029","full_name":"golikovichev/pytest-conversational","owner":"golikovichev","description":"pytest plugin for multi-turn dialogue testing. Rule-based assertions, no LLM dependency","archived":false,"fork":false,"pushed_at":"2026-06-10T12:34:38.000Z","size":201,"stargazers_count":2,"open_issues_count":5,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-10T14:18:09.783Z","etag":null,"topics":["chatbot","conversational-ai","dialogue-testing","multi-turn-dialogue","pytest-plugin","python","qa","test-automation","voice-testing"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/golikovichev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-30T11:46:53.000Z","updated_at":"2026-06-10T12:32:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/golikovichev/pytest-conversational","commit_stats":null,"previous_names":["golikovichev/pytest-conversational"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/golikovichev/pytest-conversational","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/golikovichev%2Fpytest-conversational","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/golikovichev%2Fpytest-conversational/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/golikovichev%2Fpytest-conversational/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/golikovichev%2Fpytest-conversational/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/golikovichev","download_url":"https://codeload.github.com/golikovichev/pytest-conversational/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/golikovichev%2Fpytest-conversational/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34351451,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chatbot","conversational-ai","dialogue-testing","multi-turn-dialogue","pytest-plugin","python","qa","test-automation","voice-testing"],"created_at":"2026-05-29T16:00:25.046Z","updated_at":"2026-06-15T07:01:42.297Z","avatar_url":"https://github.com/golikovichev.png","language":"Python","funding_links":[],"categories":["Testing Frameworks"],"sub_categories":[],"readme":"# pytest-conversational\n\n[![CI](https://github.com/golikovichev/pytest-conversational/actions/workflows/ci.yml/badge.svg)](https://github.com/golikovichev/pytest-conversational/actions/workflows/ci.yml)\n[![CodeQL](https://github.com/golikovichev/pytest-conversational/actions/workflows/codeql.yml/badge.svg)](https://github.com/golikovichev/pytest-conversational/actions/workflows/codeql.yml)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13010/badge)](https://www.bestpractices.dev/projects/13010)\n[![codecov](https://codecov.io/gh/golikovichev/pytest-conversational/branch/main/graph/badge.svg)](https://codecov.io/gh/golikovichev/pytest-conversational)\n[![PyPI](https://img.shields.io/pypi/v/pytest-conversational.svg)](https://pypi.org/project/pytest-conversational/)\n[![Downloads](https://static.pepy.tech/badge/pytest-conversational/month)](https://pepy.tech/project/pytest-conversational)\n[![Python versions](https://img.shields.io/pypi/pyversions/pytest-conversational.svg)](https://pypi.org/project/pytest-conversational/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Last commit](https://img.shields.io/github/last-commit/golikovichev/pytest-conversational)](https://github.com/golikovichev/pytest-conversational/commits/main)\n\nA pytest plugin for testing chat bots, voice assistants, IVR menus. Rule-based assertions, no LLM dependency.\n\nStatus: alpha. v1.0.0 target June 2026.\n\n## Why\n\nMost chat-bot test setups fall into one of two camps. Either a pile of `requests.post` calls with hand-rolled assertions, or a heavy framework that pins you to one platform. This plugin sits in the middle: a small `Conversation` object, a callable bot adapter, and pytest fixtures that wire them together.\n\nYou bring the bot. The plugin keeps turn order and per-conversation state, then prints a transcript when an assertion fails.\n\n![A multi-turn test catching a bot that drops a slot on the final turn. The failure shows exactly what the bot said versus what the test expected.](docs/assets/pytest-conversational-failure.gif)\n\n## Install\n\n```bash\npip install pytest-conversational\n```\n\nPython 3.10 and above.\n\n## Quick start\n\n```python\ndef my_bot(text, convo):\n    if \"hello\" in text.lower():\n        return \"hi\"\n    return \"sorry, did not get that\"\n\n\ndef test_greeting(conversation_factory):\n    convo = conversation_factory(bot=my_bot)\n    convo.say(\"hello there\")\n    assert convo.last.bot == \"hi\"\n```\n\n## Multi-turn state\n\nAdapters can read `convo.state` and `convo.turns` to keep slots between turns:\n\n```python\ndef slot_filling_bot(text, convo):\n    slots = convo.state.setdefault(\"slots\", {})\n    if \"name\" not in slots:\n        slots[\"name\"] = text\n        return \"got it, what city?\"\n    if \"city\" not in slots:\n        slots[\"city\"] = text\n        return f\"hello {slots['name']} from {slots['city']}\"\n    return \"done\"\n\n\ndef test_two_slot_flow(conversation_factory):\n    convo = conversation_factory(bot=slot_filling_bot)\n    convo.say(\"Mikhail\")\n    convo.say(\"Hove\")\n    assert convo.state[\"slots\"] == {\"name\": \"Mikhail\", \"city\": \"Hove\"}\n    assert convo.last.bot == \"hello Mikhail from Hove\"\n```\n\n## HTTP webhook adapter\n\nIf your bot lives behind an HTTP endpoint, use the bundled adapter instead of writing one by hand:\n\n```bash\npip install pytest-conversational[http]\n```\n\n```python\nfrom pytest_conversational import Conversation\nfrom pytest_conversational.adapters import http_webhook\n\n\ndef test_remote_bot():\n    bot = http_webhook(\"https://my-bot.example.com/webhook\", timeout=3.0)\n    convo = Conversation(bot=bot)\n    convo.say(\"hello\")\n    assert \"hi\" in convo.last.bot.lower()\n```\n\nThe default contract: POST `{\"user\": text, \"history\": [[u, b], ...]}`, expect `200 OK` with JSON `{\"reply\": \"...\"}`. If your endpoint speaks a different shape, pass `request_builder` and `response_parser` callbacks.\n\n### Security note\n\nThe webhook URL is passed through to `httpx` as-is. If your test feeds a URL it pulled from user input, fixture data, or another untrusted source, the adapter will happily hit it. That includes internal addresses like `127.0.0.1`, `169.254.169.254` (cloud metadata service), or `10.x.x.x` inside a VPC. Pin the URL to a hard-coded value in the test, or gate it through your own allowlist before passing it in.\n\n## Matchers\n\n`expect` is a small module of assertion helpers tuned for bot replies. Each matcher raises `AssertionError` with the actual reply embedded in the message, so pytest output shows what the bot said versus what the test wanted.\n\n```python\nfrom pytest_conversational import expect\n\ndef test_replies(conversation_factory):\n    convo = conversation_factory(bot=my_bot)\n    convo.say(\"hi\")\n\n    expect.contains(convo.last.bot, \"hello\")\n    expect.not_contains(convo.last.bot, \"error\")\n    expect.regex(convo.last.bot, r\"^hello\\s+\\w+\")\n    expect.one_of(convo.last.bot, [\"hello there\", \"hi there\", \"hey\"])\n```\n\n- `contains(actual, substring, *, case_sensitive=False)`: substring search. Case-insensitive by default.\n- `not_contains(actual, substring, *, case_sensitive=False)`: the negative of `contains`. Guards against leaks, for example a bot echoing an internal error, a stack trace, or a value it was never given.\n- `regex(actual, pattern, *, flags=0)`: `re.search` semantics. Returns the match object so callers can inspect captured groups.\n- `one_of(actual, options, *, case_sensitive=False, mode=\"exact\")`: matches `actual` against a list of alternative `options`. Supports `mode=\"exact\"` (full-string match, default) and `mode=\"substring\"` (checks if any option is a substring of `actual`).\n\nUse these when bare `assert \"hello\" in convo.last.bot` would give noisy failure messages across many tests. For one-off checks, plain `assert` is still fine.\n\n## Fixtures\n\n| Fixture | Purpose |\n| --- | --- |\n| `conversation` | Empty Conversation, no adapter. Good for user-only flows. |\n| `conversation_factory` | Builder. Pass a bot callable, get a fresh Conversation. |\n\n## Public API\n\n- `Conversation(bot=None, turns=[], state={})`\n- `Conversation.say(text)`: drive a turn through the adapter, return the Turn.\n- `Conversation.add_user(text)`: append a user-only turn.\n- `Conversation.last`, `.turns`, `.history`, `.transcript()`.\n- `Turn(user, bot, metadata)`.\n- `BotAdapter = Callable[[str, Conversation], str]`.\n- `expect.contains`, `expect.not_contains`, `expect.regex`, `expect.one_of`.\n\n## Roadmap\n\n- v0.4: scenario DSL loaded from YAML or plain text fixtures.\n- v0.5: async adapter support for coroutine-based bots.\n- v1.0: 12.06.2026 release.\n\n## Licence\n\nMIT. See `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgolikovichev%2Fpytest-conversational","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgolikovichev%2Fpytest-conversational","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgolikovichev%2Fpytest-conversational/lists"}