Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,22 @@ Let's assume we want to download utility bills:
- Allow input variables (for example, choosing the YEAR to download a document from). This is currently only supported for graph generation. Input variables for code generation coming soon!
- Generate code to hit all requests in the graph to perform the desired action.

## Supported LLM Providers

| Provider | Models | API Key Env Var |
|----------|--------|-----------------|
| **OpenAI** (default) | `gpt-4o`, `o1-preview`, etc. | `OPENAI_API_KEY` |
| **[MiniMax](https://www.minimax.io)** | `MiniMax-M2.7`, `MiniMax-M2.7-highspeed` | `MINIMAX_API_KEY` |

The provider is auto-detected from available API keys, or can be set explicitly with `--llm-provider`.

## Setup

1. Set up your OpenAI [API Keys](https://platform.openai.com/account/api-keys) and add the `OPENAI_API_KEY` environment variable. (We recommend using an account with access to models that are at least as capable as OpenAI o1-mini. Models on par with OpenAI o1-preview are ideal.)
1. Set up your LLM API key:
- **OpenAI**: Set up your [API Keys](https://platform.openai.com/account/api-keys) and add the `OPENAI_API_KEY` environment variable.
- **MiniMax**: Get an API key from [MiniMax Platform](https://platform.minimax.io) and set the `MINIMAX_API_KEY` environment variable.

(We recommend using models at least as capable as OpenAI o1-mini. MiniMax-M2.7 is a great alternative with competitive performance.)
2. Install Python requirements via poetry:
```
poetry install
Expand All @@ -60,11 +73,15 @@ Let's assume we want to download utility bills:
Log into your platform and perform the desired action (such as downloading a utility bill).
6. Run Integuru:
```
# Using OpenAI (default)
poetry run integuru --prompt "download utility bills" --model <gpt-4o|o3-mini|o1|o1-mini>

# Using MiniMax
poetry run integuru --prompt "download utility bills" --llm-provider minimax
```
You can also run it via Jupyter Notebook `main.ipynb`

**Recommended to use gpt-4o as the model for graph generation as it supports function calling. Integuru will automatically switch to o1-preview for code generation if available in the user's OpenAI account.**
**Recommended to use gpt-4o (OpenAI) or MiniMax-M2.7 (MiniMax) as the model for graph generation as they support function calling. Integuru will automatically switch to the alternate model for code generation.**

## Usage

Expand All @@ -75,7 +92,8 @@ poetry run integuru --help
Usage: integuru [OPTIONS]

Options:
--model TEXT The LLM model to use (default is gpt-4o)
--model TEXT The LLM model to use (default depends on
provider)
--prompt TEXT The prompt for the model [required]
--har-path TEXT The HAR file path (default is
./network_requests.har)
Expand All @@ -86,6 +104,8 @@ Options:
Input variables in the format key value
--generate-code Whether to generate the full integration
code
--llm-provider [openai|minimax] LLM provider to use (auto-detected from
API keys if not set)
--help Show this message and exit.
```

Expand Down Expand Up @@ -132,7 +152,7 @@ We open-source unofficial APIs that we've built already. You can find them [here
Collected data is stored locally in the `network_requests.har` and `cookies.json` files.

### LLM Usage
The tool uses a cloud-based LLM (OpenAI's GPT-4o and o1-preview models).
The tool uses a cloud-based LLM. Supported providers: OpenAI (GPT-4o, o1-preview) and [MiniMax](https://www.minimax.io) (MiniMax-M2.7, MiniMax-M2.7-highspeed).

### LLM Training
The LLM is not trained or improved by the usage of this tool.
11 changes: 9 additions & 2 deletions integuru/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@click.command()
@click.option(
"--model", default="gpt-4o", help="The LLM model to use (default is gpt-4o)"
"--model", default=None, help="The LLM model to use (default depends on provider)"
)
@click.option("--prompt", required=True, help="The prompt for the model")
@click.option(
Expand Down Expand Up @@ -37,8 +37,14 @@
default=False,
help="Whether to generate the full integration code",
)
@click.option(
"--llm-provider",
default=None,
type=click.Choice(["openai", "minimax"], case_sensitive=False),
help="LLM provider to use (auto-detected from API keys if not set)",
)
def cli(
model, prompt, har_path, cookie_path, max_steps, input_variables, generate_code
model, prompt, har_path, cookie_path, max_steps, input_variables, generate_code, llm_provider
):
input_vars = dict(input_variables)
asyncio.run(
Expand All @@ -50,6 +56,7 @@ def cli(
input_variables=input_vars,
max_steps=max_steps,
to_generate_code=generate_code,
llm_provider=llm_provider,
)
)

Expand Down
20 changes: 13 additions & 7 deletions integuru/main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
from typing import List
from typing import Optional
from integuru.graph_builder import build_graph
from integuru.util.LLM import llm
from integuru.util.LLM import llm, _detect_provider, PROVIDER_PRESETS

agent = None

async def call_agent(
model: str,
model: Optional[str],
prompt: str,
har_file_path: str,
cookie_path: str,
input_variables: dict = None,
max_steps: int = 15,
to_generate_code: bool = False,
):

llm.set_default_model(model)
llm_provider: Optional[str] = None,
):
# Set provider first (before model, since it resets defaults)
provider = llm_provider or _detect_provider()
llm.set_provider(provider)

# Set model (use provider default if not specified)
if model is not None:
llm.set_default_model(model)

global agent
graph, agent = build_graph(prompt, har_file_path, cookie_path, to_generate_code)
Expand All @@ -25,7 +31,7 @@ async def call_agent(
"to_be_processed_nodes": [],
"in_process_node_dynamic_parts": [],
"action_url": "",
"input_variables": input_variables or {},
"input_variables": input_variables or {},
},
{
"recursion_limit": max_steps,
Expand Down
119 changes: 111 additions & 8 deletions integuru/util/LLM.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,125 @@
import json
import os
import re

from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage

# Provider configuration presets
PROVIDER_PRESETS = {
"openai": {
"default_model": "gpt-4o",
"alternate_model": "o1-preview",
"api_key_env": "OPENAI_API_KEY",
"base_url": None,
},
"minimax": {
"default_model": "MiniMax-M2.7",
"alternate_model": "MiniMax-M2.7-highspeed",
"api_key_env": "MINIMAX_API_KEY",
"base_url": "https://api.minimax.io/v1",
},
}

_THINK_RE = re.compile(r"<think>[\s\S]*?</think>\s*", re.DOTALL)


def _detect_provider() -> str:
"""Auto-detect the LLM provider from available API keys."""
if os.environ.get("MINIMAX_API_KEY") and not os.environ.get("OPENAI_API_KEY"):
return "minimax"
return "openai"


class MiniMaxChatOpenAI(ChatOpenAI):
"""ChatOpenAI subclass that adapts deprecated function-calling kwargs
to the tools/tool_choice format and strips <think> tags from responses."""

def invoke(self, input, config=None, *, stop=None, **kwargs):
# Convert deprecated functions/function_call to tools/tool_choice
functions = kwargs.pop("functions", None)
function_call = kwargs.pop("function_call", None)

if functions:
tools = [
{"type": "function", "function": f} for f in functions
]
kwargs["tools"] = tools
if function_call and isinstance(function_call, dict):
kwargs["tool_choice"] = {
"type": "function",
"function": {"name": function_call["name"]},
}

response = super().invoke(input, config=config, stop=stop, **kwargs)

# Strip <think> tags from content
if response.content:
response.content = _THINK_RE.sub("", response.content).strip()

# Convert tool_calls back to function_call format for compatibility
if response.tool_calls:
tc = response.tool_calls[0]
response.additional_kwargs["function_call"] = {
"name": tc["name"],
"arguments": json.dumps(tc["args"]),
}

return response


class LLMSingleton:
_instance = None
_default_model = "gpt-4o"
_provider = "openai"
_default_model = "gpt-4o"
_alternate_model = "o1-preview"

@classmethod
def _create_instance(cls, model: str):
"""Create a ChatOpenAI instance with provider-specific configuration."""
preset = PROVIDER_PRESETS.get(cls._provider, PROVIDER_PRESETS["openai"])
kwargs = {"model": model, "temperature": 1}

if preset["base_url"]:
kwargs["base_url"] = preset["base_url"]

api_key = os.environ.get(preset["api_key_env"])
if api_key:
kwargs["api_key"] = api_key

chat_cls = MiniMaxChatOpenAI if cls._provider == "minimax" else ChatOpenAI
return chat_cls(**kwargs)

@classmethod
def get_instance(cls, model: str = None):
if model is None:
model = cls._default_model

if cls._instance is None:
cls._instance = ChatOpenAI(model=model, temperature=1)
cls._instance = cls._create_instance(model)
return cls._instance

@classmethod
def set_provider(cls, provider: str):
"""Set the LLM provider and update default models accordingly.

Args:
provider: Provider name ("openai" or "minimax").

Raises:
ValueError: If the provider is not supported.
"""
if provider not in PROVIDER_PRESETS:
raise ValueError(
f"Unsupported provider: {provider}. "
f"Choose from: {list(PROVIDER_PRESETS.keys())}"
)
cls._provider = provider
preset = PROVIDER_PRESETS[provider]
cls._default_model = preset["default_model"]
cls._alternate_model = preset["alternate_model"]
cls._instance = None # Reset instance to force recreation

@classmethod
def set_default_model(cls, model: str):
"""Set the default model to use when no specific model is requested"""
Expand All @@ -28,11 +134,8 @@ def revert_to_default_model(cls):

@classmethod
def switch_to_alternate_model(cls):
"""Returns a ChatOpenAI instance configured for o1-miniss"""
# Create a new instance only if we don't have one yet
cls._instance = ChatOpenAI(model=cls._alternate_model, temperature=1)

"""Returns a ChatOpenAI instance configured for the alternate model"""
cls._instance = cls._create_instance(cls._alternate_model)
return cls._instance

llm = LLMSingleton()

Loading