Skip to main content

A correct, simple, performant, and pythonic framework for building durable AI agents

Project description

PocketJoe

LLM Agents are just agents...

  • Agents are policies
  • A policy reasons over observations and chooses a batch of actions
  • A policy can be any mix of LLM-based, human-in-the-loop, or heuristic

Semantics

An agent system using Reinforcement Learning theory with LLM semantics as first class

  • Policy: all code/logic/llm are policies
  • observations - the set of observations for the policy to reason over
  • options - a set of optional sub policies that the policy can choose
  • selected_actions - the set of concurrent actions the policy chose to take
  • Message: a shared dataclass for observations and actions that aligns with llm semantics

LLM semantics as platform semantics

In LLM APIs, everything is a Message. We adopt this as our universal unit:

  • Input: observations: list[Message] (what the policy sees)
  • Output: selected_actions: list[Message] (what the policy does)

Key insight: The runtime automatically invokes all option calls and injects the results back as observations. Your policy just returns requests; the platform handles execution.

Everything is a Policy

An LLM policy that can call other policies:

@policy_spec_mcp_tool(description="Calls OpenAI GPT-4 with tool support")
class OpenAILLMPolicy_v1(Policy):
    async def __call__(self, observations: list[Message], options: list[str]) -> list[Message]:
        """LLM policy that calls OpenAI GPT-4 with tool support.
        :param observations: List of Messages representing the conversation history + new input
        :param options: Set of allowed options the LLM can call (policy names that will map to tools)
        """
        response = await openai.chat.completions.create(
            model="gpt-4",
            messages=to_completions_messages(observations),
            tools=to_completions_tools(self.ctx, options))
        return map_response_to_messagess(response)

A simple heuristic policy:

@policy_spec_mcp_tool(description="Performs web search")
class WebSearchDdgsPolicy(Policy):
    async def __call__(self, query: str) -> list[Message]:
        """
        Performs a web search and returns results.        
        :param query: The search query string to search for
        """
        results = DDGS().text(query, max_results=5)
        results_str = "\n\n".join([f"Title: {r['title']}\nURL: {r['href']}\nSnippet: {r['body']}" for r in results])
        return [Message(
            actor=self.__class__.__name__,
            type="action_result",
            payload={"content": results_str}
        )]

An orchestrator policy that coordinates LLM + search:

@policy_spec_mcp_tool(description="Orchestrator with LLM and search")
class SearchAgent(Policy):
    ctx: "AppContext"  # Override to specify context type
    
    async def __call__(self, prompt: str) -> list[Message]:
        """
        Orchestrator that gives the LLM access to web search.
        :param prompt: The user prompt to process
        """
        system_message = Message(actor="system", type="text", 
            payload={"content": "You are an AI assistant that can use tools to help answer user questions."})
        prompt_message = Message(actor="user", type="text", payload={"content": prompt})

        history = [system_message, prompt_message]
        while True:
            selected_actions = await self.ctx.llm(observations=history, options=["web_search"])
            history.extend(selected_actions)
            if not any(msg.type == "action_call" for msg in selected_actions):
                break
        
        return history

Use AppContext for registry (gives IDE type hints):

class AppContext(BaseContext):
    def __init__(self, runner):
        super().__init__(runner)
        self.llm = self._bind(OpenAILLMPolicy_v1)
        self.web_search = self._bind(WebSearchDdgsPolicy)
        self.search_agent = self._bind(SearchAgent)

Enjoy:

async def main():
    runner = InMemoryRunner()
    ctx = AppContext(runner)
    result = await ctx.search_agent(prompt="What is the latest Python version?")
    print(f"\nFinal Result: {result[-1].payload['content']}")

Why this matters:

  • Same interface for LLM, human, heuristic policies
  • All policy parameters are optional (define what you need)
  • Type-safe composition with IDE support
  • Enables evolution: human → heuristic → LLM with no refactoring

A correct, simple, performant, and pythonic framework for building durable AI agents.

"There is no flow, only Policies and Actions."

Getting Started

Prerequisites

  • Python 3.12+

Installation

uv add pocket-joe

Or with pip:

pip install pocket-joe

To install with example dependencies:

uv add pocket-joe --extra examples
# or
pip install pocket-joe[examples]

Development Setup

git clone https://github.com/Sohojoe/pocket-joe.git
cd pocket-joe
uv sync --dev --all-extras

Running Examples

Set your API key:

export OPENAI_API_KEY=sk-...

Search Agent (ReAct)

uv run python examples/search_agent.py

YouTube Summarizer

uv run python examples/youtube_summarizer.py

Dev Status

Still in prerelease, things will change

Intial version

  • [] Tidy up code - add partly refactored code
  • [] Proper tests
  • [] Implement more examples from Pocket-Flow

Durable System:

  • [] Ledger - Temporal style 'at least once, only one result' replay semantic
  • [] Durable Storage wrapper - For long running tasks & replay
  • [] Distrubuted - worker model

Background

Inspired by PocketFlow... I loved PocketFlow but it fell short in a couple of key areas. This is my rewrite that I can actually use.

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

pocket_joe-0.1.0.2.tar.gz (69.0 kB view details)

Uploaded Source

Built Distribution

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

pocket_joe-0.1.0.2-py3-none-any.whl (11.8 kB view details)

Uploaded Python 3

File details

Details for the file pocket_joe-0.1.0.2.tar.gz.

File metadata

  • Download URL: pocket_joe-0.1.0.2.tar.gz
  • Upload date:
  • Size: 69.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pocket_joe-0.1.0.2.tar.gz
Algorithm Hash digest
SHA256 9e734b88cbce450297e7abdd92b00893e105d8fdc5a42c7ccd33feb80e7975d3
MD5 7f2658d53246afb8e21b5338e00de7fe
BLAKE2b-256 beb47e797df1d4b1d1e648b2b134bed5bcf9decc36214b19a629a8c70c6e32a4

See more details on using hashes here.

Provenance

The following attestation bundles were made for pocket_joe-0.1.0.2.tar.gz:

Publisher: publish.yml on Sohojoe/pocket-joe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pocket_joe-0.1.0.2-py3-none-any.whl.

File metadata

  • Download URL: pocket_joe-0.1.0.2-py3-none-any.whl
  • Upload date:
  • Size: 11.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pocket_joe-0.1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 4bc7b1fea32450f5958bb82f37ad5e2bd833693221437c2a61a1c83942c21645
MD5 4dd6b6c8f9c1646fb6da6bce16741133
BLAKE2b-256 2ed9cbb210974bf49ff5da3e340c2ad2f66cf57842e37fb2074bfe2c52a77888

See more details on using hashes here.

Provenance

The following attestation bundles were made for pocket_joe-0.1.0.2-py3-none-any.whl:

Publisher: publish.yml on Sohojoe/pocket-joe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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