Metadata-Version: 2.4
Name: hlr_agent
Version: 0.3.0
Summary: A library for managing nodes and agent workflows
Author: David Serrano Díaz
Author-email: David Serrano <davidsd.2704@gmail.com>
License: Creative Commons Attribution-NonCommercial 4.0 International License
        
        Copyright (c) HLR 2025
        
        You are free to:
        - Share — copy and redistribute the material in any medium or format
        - Adapt — remix, transform, and build upon the material
        
        Under the following terms:
        - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
        - NonCommercial — You may not use the material for commercial purposes.
        
        No warranties are provided. For more information, see the full license at https://creativecommons.org/licenses/by-nc/4.0/
        
Project-URL: Homepage, https://github.com/DavidFraifer/HLR
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: openai
Requires-Dist: google-genai
Dynamic: author
Dynamic: license-file
Dynamic: requires-python

# HLR (Hierarchical LLM Routing)

[![PyPI version](https://badge.fury.io/py/hlr-agent.svg)](https://badge.fury.io/py/hlr-agent)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc/4.0/)

**HLR** is a flexible Python library for creating and managing hierarchical workflows driven by nodes and powered by Large Language Models (LLMs).

Each node represents a distinct step or action in your process. It can execute custom Python functions, update a shared context, and intelligently decide the next node to transition to, either programmatically or by leveraging an LLM's understanding. This makes HLR ideal for applications needing sequential task processing with dynamic, context-aware routing.

## Features

-   **Node-Based Workflows:** Structure complex processes into manageable, reusable nodes.
-   **Hierarchical Flow:** Design workflows where nodes can branch and converge based on logic or LLM decisions.
-   **Non-Linear Dependencies:** Support for circular dependencies between nodes for complex workflows.
-   **Dynamic Routing:**
    -   Nodes can explicitly return the ID of the next node.
    -   Alternatively, let an integrated LLM choose the next node based on descriptions and the current context.
-   **Shared Context:** Maintain state across nodes using a simple dictionary (`agent.context`). Nodes can read and write data (like logs, intermediate results, or the execution path).
    -   `agent.context["info"]`: Commonly used for accumulating logs or data.
    -   `agent.context["route"]`: Automatically tracks the sequence of nodes visited.
-   **LLM Integration:** Seamlessly uses Google's Gemini models (`gemini-2.0-flash`, `gemini-1.5-flash`, `gemini-1.5-flash-8b`) and OpenAI (`gpt-4o`) for routing decisions.
-   **Flexible Message Handling:** Pass user messages at runtime through the `run()` method for dynamic execution.
-   **Robust Validation:** Ensures required parameters are provided during initialization and runtime, preventing common errors.

## Installation

Install HLR directly from PyPI:

```bash
pip install hlr-agent
```

*(Note: PyPI normalizes names, so `pip install hlr_agent` also works).*

## Usage Example

Here's how to define nodes, functions, and run the agent:

### 1. Define Node Functions

Each function associated with a node receives the `agent` instance, allowing it to access and modify `agent.context`.

```python
# example.py
from hlr_agent import Node, Agent
import time

# Example context keys used: "info", "route", "emails_to_send"



def func_database(agent):
    print("Querying database...")
    found_emails = [# Simulate database query result
        "user1@example.com",
        "test.user@sample.net",
        "another@domain.org"
    ]
    print(f"Database: Found {len(found_emails)} emails.")
    agent.context["info"] += "\nQuery result:\n"+ str(found_emails)


def func_mailing(agent): 
    print("Emails has been formatted.")
    redacted_emails = [ # Simulate redaction of emails
        ["Subject1", "Message1", "user1@example.com"],
        ["Subject2", "Message2", "test.user@sample.net"],
        ["Subject3", "Message3", "another@domain.org"],
    ]
    agent.context["info"] += "\nRedacted emails:\n" + str(redacted_emails)
    print("Emails has been sent.")


def func_output(agent):
    if "info" in agent.context: 
        print("LOGS:\n" + agent.context["info"])
    if "route" in agent.context:
        print(agent.context["route"]) 

```

### 2. Configure Nodes

Define the structure of your workflow using `Node` objects. Nodes can have circular dependencies for complex workflows:

```python
# example.py (continued)

nodes = [
    Node("Input", children=["Database", "Mailing", "None"]),
    Node("Database", children=["Mailing","Output"], func=func_database, description="Select this if the user wants to use a database"),
    Node("Mailing", children=["Database","Output"], func=func_mailing, description="Select this if the user wants to use a mailing related functionality."),
    Node("None", children=["Output"], description="Select this node if the rest o the nodes are not valid for the request"),
    Node("Output", children=None, func=func_output),
]
```

### 3. Initialize and Run the Agent

Create an `Agent` instance and execute the workflow using `agent.run()`. The user message is now passed directly to the `run()` method:

```python
# example.py (continued)

# --- Agent Initialization ---
agent = Agent(
    nodes=nodes,
    start_node_id="Input",
    end_node_id="Output",
    model="gemini-1.5-flash-8b", # Available models: gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-flash-8b, gpt-4o
    api_key="YOUR_GEMINI_API_KEY" # Replace with your actual API key
)

# --- Run the Agent ---
user_request = "Please get the emails from the database and send them a welcome message."

print("--- Starting Agent Run ---")
try:
    agent.run(user_message=user_request)  # Message is passed here, not in init
except ValueError as e:
    print(f"Agent Error: {e}")
print("\n--- Agent Run Finished ---")

```

## How It Works

1.  **Initialization (`Agent(...)`):**
    *   Validates all required parameters (`nodes`, `start_node_id`, `end_node_id`, `model`, `api_key`).
    *   Checks for duplicate `node_id`s.
    *   Builds an internal dictionary of nodes for quick access.
    *   Initializes `agent.context` (e.g., setting the initial `route`).

2.  **Execution (`agent.run(user_message=...)`):**
    *   Takes the `user_message` for the current run.
    *   Starts at the `start_node_id`.
    *   Enters a loop that continues until a terminal node is reached or the step limit is exceeded.
    *   **In each step:**
        *   Executes the `func` of the current node (if defined). This function can modify `agent.context`.
        *   Determines the `next_node_id`:
            *   **Explicit:** If `func` returns a valid node ID, use that.
            *   **Implicit/LLM:** If `func` returns `None`:
                *   If the current node has no children, the flow ends.
                *   If it has one valid child (with description or is `end_node_id`), transition to it.
                *   If multiple valid children exist, call the LLM (`get_next_node`).
                    *   The LLM receives: the list of valid child IDs and their descriptions, the `user_message` for the run, and `extra_context` (a string combining `agent.context["route"]` and `agent.context["info"]`).
                    *   The LLM returns the chosen child ID.
        *   Updates `agent.context["route"]` by appending the `next_node_id`.
        *   Sets the `current_id` to the `next_node_id` for the next iteration.

3.  **Termination:** The loop ends when `current_id` becomes `None` (either explicitly set by a node, reaching a node with no children, or encountering an error like an invalid explicit return). The `func_output` (or the function of the last node) typically handles final reporting.

## What's New in v0.3.0

-   **Runtime Message Passing:** User messages are now passed to `agent.run(user_message="...")` instead of during agent initialization, allowing for more flexible and dynamic execution.
-   **Additional Gemini Models:** Added support for `gemini-1.5-flash` and `gemini-1.5-flash-8b` alongside the existing `gemini-2.0-flash` and `gpt-4o`.
-   **Enhanced Non-Linear Workflows:** Improved support for circular dependencies between nodes, enabling more complex workflow patterns where nodes can reference each other in cycles.
-   **Better Circular Dependency Handling:** The routing system now better handles cases where nodes have bidirectional relationships for sophisticated task management.

## Contributing

Contributions, issues, and feature requests are welcome! Feel free to check the [issues page](https://github.com/DavidFraifer/HLR/issues) or submit a pull request.

## License

This project is licensed under the [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0/).

## Contact

David Serrano Díaz – davidsd.2704@gmail.com

Project Link: [https://github.com/DavidFraifer/HLR](https://github.com/DavidFraifer/HLR)
