A provider-agnostic, plugin-based framework for building AI applications and agents. Declare what you need -- providers, tools, capabilities -- not which SDK provides it.
Project description
Requisite
Declare what your AI application needs — not which SDK provides it.
A provider-agnostic, plugin-based Python framework for building AI
applications and agents. Swap the LLM provider, the multi-agent execution
engine, or the implementation behind a capability like "weather" or
"internet_search" — all via configuration, never a rewrite.
from requisite import AI
ai = AI() # provider="openai" by default
ai = AI(provider="anthropic", model="claude-sonnet-4-6") # same API, different provider
ai = AI(provider="gemini", model="gemini-2.5-flash")
ai = AI(provider="groq", model="llama-3.3-70b-versatile")
from requisite import Agent
agent = Agent(name="Assistant", provider="openai")
agent.requires("weather", "internet_search", "filesystem") # not use_tool(specific_impl)
agent.run("What's the weather in Tokyo?")
Install
pip install -e .[all] # every provider + langgraph
pip install -e .[openai] # OpenAI only
pip install -e .[anthropic] # Anthropic (Claude) only
pip install -e .[gemini] # Gemini only
pip install -e .[groq] # Groq only (uses the openai package -- wire-compatible)
pip install -e .[azure_openai] # Azure OpenAI only (uses the openai package)
pip install -e .[langgraph] # native + langgraph orchestration
Or with the plain requirements file: pip install -r requirements.txt
Note on the Gemini SDK: this framework uses the current, unified
google-genaipackage (from google import genai). Do not install the deprecatedgoogle-generativeaipackage — the two conflict.Note on Azure OpenAI: uses Azure's current v1 GA API (the plain
openaiclient pointed at your endpoint) — no separate SDK, no datedapi-versionstring. See ADR-0002.
Configuration
Copy .env.example to .env and fill in the key(s) for the provider(s) you use:
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GEMINI_API_KEY=
GROQ_API_KEY=
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_ENDPOINT=
DEFAULT_PROVIDER=openai
MODEL=gpt-4o-mini
TEMPERATURE=0.2
Settings (requisite/config/settings.py) reads this automatically — you
never call os.environ.get yourself. .env.example also reserves
placeholders for planned integrations (GitHub, Hugging Face, AWS, Azure
general-purpose credentials, Pinecone, Weaviate) — see ROADMAP.md.
Usage
Chat
from requisite import AI
ai = AI()
print(ai.chat("Explain LangGraph in one sentence."))
Supported providers
| Provider | provider= |
Model examples | Notes |
|---|---|---|---|
| OpenAI | "openai" |
gpt-4o-mini, gpt-4o |
|
| Anthropic | "anthropic" |
claude-sonnet-4-6, claude-opus-4-8 |
Native structured output via messages.parse |
| Gemini | "gemini" |
gemini-2.5-flash, gemini-2.5-pro |
Uses the unified google-genai SDK |
| Groq | "groq" |
llama-3.3-70b-versatile, openai/gpt-oss-20b |
OpenAI-wire-compatible; uses the openai package |
| Azure OpenAI | "azure_openai" |
your deployment name | Requires azure_endpoint (or AZURE_OPENAI_ENDPOINT); current v1 GA API, no api-version needed |
Switching between any of these is the provider=/AZURE_OPENAI_ENDPOINT
change shown above — no other code changes. See
ADR-0002 for
why Groq and Azure OpenAI are implemented as thin OpenAIProvider
subclasses rather than separate SDKs.
Structured output
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
person = ai.chat("Extract: John is 30 years old.", response_model=Person)
print(person.name, person.age) # John 30
Tool calling
from requisite.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"Sunny, 22C in {city}"
response = ai.chat_response("What's the weather in Paris?", tools=[get_weather])
if response.has_tool_calls:
call = response.tool_calls[0]
result = get_weather.tool.execute(**call.arguments)
That's the low-level view. For most applications, let an Agent run the
full tool-calling loop for you (see below).
Agents
from requisite import Agent
from requisite.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"Sunny, 22C in {city}"
agent = Agent(name="Weather Agent", provider="openai", tools=[get_weather])
result = agent.run("What's the weather in Paris?")
print(result.content) # "It's sunny and 22C in Paris."
print(result.tool_calls_executed) # ["get_weather"]
Agent automatically: offers its tools/skills to the model, executes any
tool calls the model requests, feeds results back, and repeats (up to
max_iterations) until it has a final answer.
Multi-agent workflows
from requisite import Agent, Workflow
research = Agent(name="Researcher", provider="openai")
writer = Agent(name="Writer", provider="openai")
workflow = Workflow()
workflow.add(research)
workflow.add(writer)
result = workflow.run("Research AI trends and write a summary.")
print(result.content)
Run agents in parallel against the same input instead of as a pipeline:
workflow.parallel()
result = workflow.run("What is retrieval-augmented generation?")
Switch the execution engine — the .add() / .run() API never changes:
workflow.use_langgraph() # requires: pip install langgraph
result = workflow.run("Research AI trends and write a summary.")
workflow.use_native() # back to the built-in, dependency-free engine
Skills
A skill is a reusable, higher-level capability (vs. a tool, which is typically a single function). Skills expose themselves to the model as tools automatically:
from requisite.skills import BaseSkill
class ReadFileSkill(BaseSkill):
def __init__(self):
super().__init__(name="read_file", description="Read a text file's contents.")
def run(self, path: str) -> str:
with open(path) as f:
return f.read()
agent = Agent(name="File Agent", provider="openai", skills=[ReadFileSkill()])
Capabilities: declare what, not which
agent.use_tool(specific_impl) binds an agent to one concrete
implementation at write-time. agent.requires("weather") binds it to a
name, resolved at runtime against whichever implementation is
currently available — a native tool, an MCP server, a cloud API, or a
third-party plugin:
from requisite import Agent
agent = Agent(name="Assistant", provider="openai")
agent.requires("weather", "internet_search", "filesystem")
result = agent.run("What's the weather in Tokyo?")
Three capabilities ship out of the box, backed by free/keyless APIs and
the local filesystem (see requisite/capabilities/resolvers.py):
"filesystem", "weather", "internet_search".
Register a better provider for the same capability at a higher priority and it takes over automatically — application code never changes:
from requisite.capabilities import default_registry
default_registry.register(
"weather",
my_paid_weather_tool,
provider_name="acme-weather",
priority=10,
is_available=lambda: bool(os.environ.get("ACME_API_KEY")),
)
# agent.requires("weather") now resolves to "acme-weather" when the key
# is set, and quietly falls back to the built-in provider otherwise.
This is the same interface + registry pattern used for providers and
orchestrators, one layer up: CapabilityRegistry.resolve(name) picks the
highest-priority provider whose is_available() currently returns
True, raising CapabilityException if none are.
Memory: conversation history across separate calls
from requisite import Agent
from requisite.memory import InProcessMemory
memory = InProcessMemory()
agent = Agent(name="Assistant", provider="openai", memory=memory, session_id="user-42")
agent.run("My name is Alex.")
result = agent.run("What's my name?") # remembers "Alex" via `memory`
print(result.content)
session_id is required whenever memory is set — there's no implicit
"current user," so an agent with memory but no session_id fails at
construction time rather than silently sharing one conversation across
callers. When memory is configured, run()/arun() must be called with
a plain string (the new turn); prior history is loaded from memory
automatically. Only the user's turn and the agent's final answer are
persisted, not intermediate tool-call round-trips — see
ADR-0002 for
the reasoning.
InProcessMemory (dict-backed, lost on restart) is the zero-dependency
default. Implement requisite.memory.base.BaseMemory for a persistent
backend (Redis, SQLite, ...) — see ROADMAP.md.
Conversation policies: keeping long histories bounded
A conversation that grows unbounded eventually blows past the model's
context window (or just gets expensive). conversation_policy= trims or
summarizes history once, before each run()/arun() call — independent
of whether memory is configured:
from requisite import Agent
from requisite.memory import InProcessMemory, MessageCountPolicy
agent = Agent(
name="Assistant",
provider="openai",
memory=InProcessMemory(),
session_id="user-42",
conversation_policy=MessageCountPolicy(max_messages=20), # keep the most recent 20
)
For long-running conversations where you'd rather compress old context
than drop it, SummarizingPolicy collapses older messages into one
LLM-generated summary, keeping the most recent few verbatim:
from requisite import AI
from requisite.memory import SummarizingPolicy
# A separate, cheaper AI instance for summarization is a reasonable choice --
# summarization quality requirements are usually lower than the agent's own task.
summarizer = AI(provider="groq", model="llama-3.3-70b-versatile")
agent = Agent(
name="Assistant",
provider="openai",
memory=InProcessMemory(),
session_id="user-42",
conversation_policy=SummarizingPolicy(summarizer, max_messages=20, keep_recent=6),
)
See ADR-0003
for why the policy is applied once per call rather than mid-tool-loop,
and why it doesn't change what gets persisted to memory.
Prompt templates
from requisite.prompts import ChatPromptTemplate
chat_template = ChatPromptTemplate.from_messages([
("system", "You are a {persona}."),
("user", "{question}"),
])
messages = chat_template.format_messages(persona="pirate", question="Where's the treasure?")
print(ai.chat(messages))
ChatPromptTemplate.format_messages(...) returns a plain list[Message]
— pass it to ai.chat(...) or agent.run(...) exactly like any other
message sequence. For a single string instead of a full conversation,
use PromptTemplate:
from requisite.prompts import PromptTemplate
translate = PromptTemplate.from_template("Translate to {language}: {text}")
print(ai.chat(translate.format(language="French", text="Good morning")))
# Pre-fill some variables now, leave the rest for later:
french_translator = translate.partial(language="French")
print(ai.chat(french_translator.format(text="Good night")))
Register named templates for reuse across an application with
PromptTemplateRegistry.
Structured logging
Every framework module logs through the standard library
(logging.getLogger("requisite.<subpackage>")). Opt into JSON output
with one call, wherever your application configures logging — this is
never done automatically by the framework:
from requisite.telemetry import configure_logging
configure_logging(level="INFO", json_format=True)
{"timestamp": "...", "level": "DEBUG", "logger": "requisite.capabilities", "message": "Resolved capability 'weather' -> 'open-meteo'", "capability": "weather", "provider_name": "open-meteo"}
Any extra={...} fields passed to a log call are merged into the JSON
payload automatically — the same log call produces a readable line with
the default formatter and a structured payload with json_format=True.
Streaming & async
for token in ai.stream("Write a haiku about distributed systems."):
print(token, end="")
text = await ai.achat("Hello!")
async for token in ai.astream("Hello, streamed!"):
print(token, end="")
result = await agent.arun("What's the weather in Paris?")
result = await workflow.arun("Research AI trends.")
Conversation history
from requisite import Message
history = [
Message.user("My name is Alex."),
Message.assistant("Nice to meet you, Alex!"),
Message.user("What's my name?"),
]
print(ai.chat(history))
See examples/ for complete, runnable scripts covering each of the above.
Architecture
requisite/
├── core/ # Provider-agnostic data models (Message, ChatResponse,
│ # ToolCall, ...) and the AIException hierarchy
├── config/ # Settings (pydantic-settings, reads .env)
├── providers/ # BaseProvider interface + OpenAI, Anthropic, Gemini, Groq,
│ # Azure OpenAI + ProviderRegistry (extensible, DI-friendly)
├── tools/ # Tool, @tool decorator, ToolRegistry, JSON Schema derivation
├── skills/ # BaseSkill, SkillRegistry -- reusable higher-level capabilities
├── capabilities/ # CapabilityRegistry -- resolve a named capability (e.g.
│ # "weather") to whichever implementation is available
├── memory/ # BaseMemory + InProcessMemory + MemoryRegistry, plus
│ # BaseConversationPolicy (MessageCountPolicy, SummarizingPolicy)
├── prompts/ # PromptTemplate, ChatPromptTemplate, PromptTemplateRegistry
├── telemetry/ # Structured (JSON) logging -- opt-in, never automatic
├── agents/ # Agent (tool-calling loop, .requires(), optional memory) + AgentRegistry
├── orchestrators/ # BaseOrchestrator interface + native (sequential/parallel)
│ # and langgraph backends + OrchestratorRegistry
├── workflows/ # Workflow -- the small, ergonomic multi-agent facade
└── ai.py # The `AI` facade -- the class most users touch directly
Every layer follows the same pattern: a small abstract interface
(BaseProvider, BaseOrchestrator, ...), one or more concrete
implementations, and a plain, instantiable registry (not a singleton)
mapping names to constructors. That's what makes each of the following a
configuration change rather than a code change:
AI(provider="openai")→AI(provider="gemini")Workflow(orchestrator="native")→workflow.use_langgraph()agent.use_tool(specific_impl)→agent.requires("weather")
See ARCHITECTURE.md for the full dependency
diagram, request-flow walkthroughs (a chat call, an agent's tool-calling
loop, capability resolution, a multi-agent workflow), and the design
decisions behind them. See CONTRIBUTING.md for
the step-by-step to add a new provider, orchestrator backend, or
capability resolver.
Error handling
All framework exceptions inherit from AIException:
AIException
├── ConfigurationException # missing/invalid config, unknown provider/orchestrator name
├── ProviderException # provider SDK call failed (wraps the original error)
├── ToolException # a tool wasn't found, or raised while executing
├── SkillException # a skill wasn't found, or raised while executing
├── AgentException # agent execution failed (e.g. max_iterations exceeded)
├── CapabilityException # a required capability has no available provider
├── PromptException # a prompt template was rendered without a required variable
└── MCPException # reserved for the upcoming MCP integration
Provider SDK errors are never swallowed — they're wrapped with provider
and original_error attached, and re-raised via raise ... from original_error
so the original traceback is preserved.
Testing
pytest
Tests never hit the network: the OpenAI and Gemini SDKs are faked via
sys.modules injection, and the AI / Agent / Workflow facades are
tested against fully in-memory fake providers.
Roadmap
Implemented: 5 providers (OpenAI, Anthropic, Gemini, Groq, Azure OpenAI),
structured outputs, tool calling, skills, capability resolution
(agent.requires(...)), memory + conversation policies
(Agent(memory=..., conversation_policy=...)), prompt templates,
structured logging, agents + registry, multi-agent workflows
(sequential/parallel, native + langgraph backends).
See ROADMAP.md for the full, per-layer status table
(providers, orchestration strategies, MCP, memory, RAG, ...) and what's
explicitly out of scope. See FEATURES.md for the same
information organized as a line-by-line checklist against the original
project vision.
Contributing
Contributions are welcome:
CONTRIBUTING.md— dev setup, running checks, and step-by-step guides for adding a provider, orchestrator backend, or capability resolver.ARCHITECTURE.md— how the framework fits together and why: the interface + registry pattern, request-flow walkthroughs, design decisions.docs/adr/— Architecture Decision Records: the formal rationale behind core interfaces, extension points, plugin discovery, configuration model, and therequisite-corevs. optional-integrations boundary. Start with ADR-0001.DEVELOPMENT.md— coding standards, testing philosophy, docstring format, logging/error-handling conventions, versioning policy.ROADMAP.md— what's shipped, planned, or out of scope.
Please also read the Code of Conduct. Security
issues should go through SECURITY.md, not a public issue.
See CHANGELOG.md for release history.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file requisite_ai-0.1.0.tar.gz.
File metadata
- Download URL: requisite_ai-0.1.0.tar.gz
- Upload date:
- Size: 79.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3cceef19affaef3361e37e109422f82c56dcc2e2f465b6a80da6d4c1f9351d65
|
|
| MD5 |
5a9a61eb5101d01bc0af63831afdf2e8
|
|
| BLAKE2b-256 |
f77d1524e22027400f05cc82358395ec167dbb8b563b1aca4c324094616b7c9b
|
Provenance
The following attestation bundles were made for requisite_ai-0.1.0.tar.gz:
Publisher:
publish.yml on requisite-ai/requisite-ai
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
requisite_ai-0.1.0.tar.gz -
Subject digest:
3cceef19affaef3361e37e109422f82c56dcc2e2f465b6a80da6d4c1f9351d65 - Sigstore transparency entry: 2174212234
- Sigstore integration time:
-
Permalink:
requisite-ai/requisite-ai@47961023af5bef222caf2f60201f25749a836773 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/requisite-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@47961023af5bef222caf2f60201f25749a836773 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file requisite_ai-0.1.0-py3-none-any.whl.
File metadata
- Download URL: requisite_ai-0.1.0-py3-none-any.whl
- Upload date:
- Size: 82.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43cdd8082fa49bf906cc701fc0002d198ac6ddd5722f8c683704c392b23dc628
|
|
| MD5 |
1f0fcbd69c8df04bb1c791e0be05819c
|
|
| BLAKE2b-256 |
21b6fdbefd1b8238be88bf8470cc61879ca592bcff1d82a00f5eb1cfca4587c6
|
Provenance
The following attestation bundles were made for requisite_ai-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on requisite-ai/requisite-ai
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
requisite_ai-0.1.0-py3-none-any.whl -
Subject digest:
43cdd8082fa49bf906cc701fc0002d198ac6ddd5722f8c683704c392b23dc628 - Sigstore transparency entry: 2174212417
- Sigstore integration time:
-
Permalink:
requisite-ai/requisite-ai@47961023af5bef222caf2f60201f25749a836773 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/requisite-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@47961023af5bef222caf2f60201f25749a836773 -
Trigger Event:
workflow_dispatch
-
Statement type: