Add MiniMax LLM integration and local .env support
- Add .env to .gitignore (API keys stay local) - Add LLM client with MiniMax and OpenAI support - Update config to load from environment variables - Wire up Architect agent to actually call the LLM - Add MiniMax API key to local .env file
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# Dependencies
|
||||||
|
__pycache__/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.eggs/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Local environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Output
|
||||||
|
output/
|
||||||
|
*.log
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
"""Base agent class for Opus Orchestrator."""
|
"""Base agent class for Opus Orchestrator."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Generic, TypeVar
|
from typing import Any, Generic, Optional, TypeVar
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from opus_orchestrator.config import AgentConfig, get_config
|
from opus_orchestrator.config import AgentConfig, get_config
|
||||||
|
from opus_orchestrator.utils.llm import LLMClient, get_llm_client
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
T = TypeVar("T", bound=BaseModel)
|
||||||
@@ -23,9 +24,6 @@ class AgentResponse(BaseModel):
|
|||||||
arbitrary_types_allowed = True
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAgent(ABC, Generic[T]):
|
class BaseAgent(ABC, Generic[T]):
|
||||||
"""Base class for all Opus agents.
|
"""Base class for all Opus agents.
|
||||||
|
|
||||||
@@ -49,6 +47,14 @@ class BaseAgent(ABC, Generic[T]):
|
|||||||
self.system_prompt = system_prompt
|
self.system_prompt = system_prompt
|
||||||
self.output_schema = output_schema
|
self.output_schema = output_schema
|
||||||
self.config = config or get_config().agent
|
self.config = config or get_config().agent
|
||||||
|
self._llm_client: Optional[LLMClient] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def llm_client(self) -> LLMClient:
|
||||||
|
"""Get or create LLM client."""
|
||||||
|
if self._llm_client is None:
|
||||||
|
self._llm_client = get_llm_client()
|
||||||
|
return self._llm_client
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse:
|
async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse:
|
||||||
@@ -63,6 +69,31 @@ class BaseAgent(ABC, Generic[T]):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def call_llm(
|
||||||
|
self,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
temperature: Optional[float] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Call the LLM with prompts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_prompt: System prompt
|
||||||
|
user_prompt: User prompt
|
||||||
|
temperature: Optional temperature override
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated text
|
||||||
|
"""
|
||||||
|
temp = temperature if temperature is not None else self.config.temperature
|
||||||
|
|
||||||
|
return await self.llm_client.complete(
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
user_prompt=user_prompt,
|
||||||
|
temperature=temp,
|
||||||
|
max_tokens=self.config.max_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
def build_system_prompt(self, context: dict[str, Any]) -> str:
|
def build_system_prompt(self, context: dict[str, Any]) -> str:
|
||||||
"""Build the full system prompt with context.
|
"""Build the full system prompt with context.
|
||||||
|
|
||||||
@@ -104,3 +135,9 @@ class BaseAgent(ABC, Generic[T]):
|
|||||||
|
|
||||||
Please complete this task following the methodology specified in your system prompt.
|
Please complete this task following the methodology specified in your system prompt.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
"""Clean up resources."""
|
||||||
|
if self._llm_client:
|
||||||
|
await self._llm_client.close()
|
||||||
|
self._llm_client = None
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ class ArchitectAgent(BaseAgent):
|
|||||||
Returns:
|
Returns:
|
||||||
AgentResponse with BookBlueprint
|
AgentResponse with BookBlueprint
|
||||||
"""
|
"""
|
||||||
# This is a placeholder - actual implementation would call the LLM
|
|
||||||
# For now, we'll structure the prompt
|
|
||||||
raw_content = input_data.get("raw_content", "")
|
raw_content = input_data.get("raw_content", "")
|
||||||
intent = input_data.get("intent", {})
|
intent = input_data.get("intent", {})
|
||||||
genre = intent.get("genre", "general")
|
genre = intent.get("genre", "general")
|
||||||
@@ -107,23 +105,34 @@ class ArchitectAgent(BaseAgent):
|
|||||||
|
|
||||||
Generate a complete story blueprint following the Architect's methodology.
|
Generate a complete story blueprint following the Architect's methodology.
|
||||||
Include all sections specified in your system prompt.
|
Include all sections specified in your system prompt.
|
||||||
|
|
||||||
|
Be specific and detailed. The blueprint should be comprehensive enough that another agent could write each chapter from it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# In actual implementation, this would call the LLM
|
try:
|
||||||
# For now, return a structured response
|
# Call the LLM
|
||||||
return AgentResponse(
|
result = await self.call_llm(
|
||||||
success=True,
|
system_prompt=self.build_system_prompt(context),
|
||||||
output={
|
user_prompt=user_prompt,
|
||||||
"status": "blueprint_generated",
|
)
|
||||||
"message": "Blueprint generation would be executed here with LLM",
|
|
||||||
},
|
return AgentResponse(
|
||||||
metadata={
|
success=True,
|
||||||
"role": "Architect",
|
output=result,
|
||||||
"input_word_count": len(raw_content.split()),
|
metadata={
|
||||||
"target_word_count": target_word_count,
|
"role": "Architect",
|
||||||
"genre": genre,
|
"input_word_count": len(raw_content.split()),
|
||||||
},
|
"target_word_count": target_word_count,
|
||||||
)
|
"genre": genre,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return AgentResponse(
|
||||||
|
success=False,
|
||||||
|
output=None,
|
||||||
|
error=str(e),
|
||||||
|
metadata={"role": "Architect"},
|
||||||
|
)
|
||||||
|
|
||||||
async def expand_chapter(
|
async def expand_chapter(
|
||||||
self,
|
self,
|
||||||
@@ -157,13 +166,31 @@ Include all sections specified in your system prompt.
|
|||||||
|
|
||||||
Expand this chapter beat into a detailed scene specification following
|
Expand this chapter beat into a detailed scene specification following
|
||||||
Template B from the Fiction Fortress methodology.
|
Template B from the Fiction Fortress methodology.
|
||||||
|
|
||||||
|
Include:
|
||||||
|
1. Opening beat - how the scene opens
|
||||||
|
2. Conflict beat - what escalates tension
|
||||||
|
3. Turn beat - what changes the situation
|
||||||
|
4. Ending beat - what hook or change ends the scene
|
||||||
|
|
||||||
|
Be specific about character motivations, dialogue objectives, and emotional progression.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return AgentResponse(
|
try:
|
||||||
success=True,
|
result = await self.call_llm(
|
||||||
output={
|
system_prompt=self.build_system_prompt(context),
|
||||||
"status": "chapter_expanded",
|
user_prompt=user_prompt,
|
||||||
"chapter_number": chapter.chapter_number,
|
)
|
||||||
},
|
|
||||||
metadata={"role": "Architect", "task": "chapter_expansion"},
|
return AgentResponse(
|
||||||
)
|
success=True,
|
||||||
|
output=result,
|
||||||
|
metadata={"role": "Architect", "task": "chapter_expansion"},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return AgentResponse(
|
||||||
|
success=False,
|
||||||
|
output=None,
|
||||||
|
error=str(e),
|
||||||
|
metadata={"role": "Architect"},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
"""Opus Orchestrator AI - Configuration."""
|
"""Opus Orchestrator AI - Configuration."""
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
def _load_env(key: str, default: Optional[str] = None) -> Optional[str]:
|
||||||
|
"""Load from environment variable."""
|
||||||
|
return os.environ.get(key, default)
|
||||||
|
|
||||||
|
|
||||||
class FortressConfig(BaseModel):
|
class FortressConfig(BaseModel):
|
||||||
"""Configuration for Fortress integration."""
|
"""Configuration for Fortress integration."""
|
||||||
|
|
||||||
@@ -18,10 +24,14 @@ class FortressConfig(BaseModel):
|
|||||||
class AgentConfig(BaseModel):
|
class AgentConfig(BaseModel):
|
||||||
"""Configuration for AI agents."""
|
"""Configuration for AI agents."""
|
||||||
|
|
||||||
model: str = Field(default="gpt-4o", description="Default model for agents")
|
model: str = Field(default="MiniMax/MiniMax-M2.1", description="Default model for agents")
|
||||||
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
|
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
|
||||||
max_tokens: Optional[int] = Field(default=None, description="Max tokens per response")
|
max_tokens: Optional[int] = Field(default=None, description="Max tokens per response")
|
||||||
max_iterations: int = Field(default=10, description="Max iterations per agent task")
|
max_iterations: int = Field(default=10, description="Max iterations per agent task")
|
||||||
|
|
||||||
|
# Provider configuration
|
||||||
|
provider: str = Field(default="minimax", description="LLM provider: minimax, openai, anthropic")
|
||||||
|
api_key: Optional[str] = Field(default=None, description="API key for LLM provider")
|
||||||
|
|
||||||
|
|
||||||
class IterationConfig(BaseModel):
|
class IterationConfig(BaseModel):
|
||||||
@@ -57,6 +67,37 @@ class OpusConfig(BaseModel):
|
|||||||
frozen = False
|
frozen = False
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_from_env() -> OpusConfig:
|
||||||
|
"""Load configuration from environment variables.
|
||||||
|
|
||||||
|
Reads:
|
||||||
|
- MINIMAX_API_KEY or OPENAI_API_KEY for LLM
|
||||||
|
- GITHUB_TOKEN for GitHub operations
|
||||||
|
"""
|
||||||
|
# Load API keys
|
||||||
|
api_key = _load_env("MINIMAX_API_KEY") or _load_env("OPENAI_API_KEY")
|
||||||
|
github_token = _load_env("GITHUB_TOKEN")
|
||||||
|
|
||||||
|
# Determine provider
|
||||||
|
if _load_env("MINIMAX_API_KEY"):
|
||||||
|
provider = "minimax"
|
||||||
|
default_model = "MiniMax/MiniMax-M2.1"
|
||||||
|
else:
|
||||||
|
provider = "openai"
|
||||||
|
default_model = "gpt-4o"
|
||||||
|
|
||||||
|
agent_config = AgentConfig(
|
||||||
|
model=default_model,
|
||||||
|
provider=provider,
|
||||||
|
api_key=api_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
return OpusConfig(
|
||||||
|
agent=agent_config,
|
||||||
|
github_token=github_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Global config instance
|
# Global config instance
|
||||||
_config: Optional[OpusConfig] = None
|
_config: Optional[OpusConfig] = None
|
||||||
|
|
||||||
@@ -65,7 +106,11 @@ def get_config() -> OpusConfig:
|
|||||||
"""Get the global configuration instance."""
|
"""Get the global configuration instance."""
|
||||||
global _config
|
global _config
|
||||||
if _config is None:
|
if _config is None:
|
||||||
_config = OpusConfig()
|
# Try to load from environment
|
||||||
|
try:
|
||||||
|
_config = load_config_from_env()
|
||||||
|
except Exception:
|
||||||
|
_config = OpusConfig()
|
||||||
return _config
|
return _config
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""LLM client for Opus Orchestrator.
|
||||||
|
|
||||||
|
Supports MiniMax and OpenAI providers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class LLMClient:
|
||||||
|
"""Simple LLM client for making API calls."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
provider: str = "minimax",
|
||||||
|
model: str = "MiniMax/MiniMax-M2.1",
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""Initialize LLM client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: API key for the provider
|
||||||
|
provider: Provider name (minimax, openai, anthropic)
|
||||||
|
model: Model identifier
|
||||||
|
base_url: Optional custom base URL
|
||||||
|
"""
|
||||||
|
self.api_key = api_key or os.environ.get("MINIMAX_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
||||||
|
self.provider = provider
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
# Set base URL based on provider
|
||||||
|
if base_url:
|
||||||
|
self.base_url = base_url
|
||||||
|
elif provider == "minimax":
|
||||||
|
self.base_url = "https://api.minimax.chat/v1"
|
||||||
|
elif provider == "openai":
|
||||||
|
self.base_url = "https://api.openai.com/v1"
|
||||||
|
else:
|
||||||
|
self.base_url = "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
self.client = httpx.AsyncClient(timeout=120.0)
|
||||||
|
|
||||||
|
async def complete(
|
||||||
|
self,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
temperature: float = 0.7,
|
||||||
|
max_tokens: Optional[int] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Make a completion request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_prompt: System prompt
|
||||||
|
user_prompt: User prompt
|
||||||
|
temperature: Sampling temperature
|
||||||
|
max_tokens: Maximum tokens to generate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generated text
|
||||||
|
"""
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.provider == "minimax":
|
||||||
|
return await self._complete_minimax(
|
||||||
|
system_prompt, user_prompt, temperature, max_tokens, headers
|
||||||
|
)
|
||||||
|
elif self.provider == "openai":
|
||||||
|
return await self._complete_openai(
|
||||||
|
system_prompt, user_prompt, temperature, max_tokens, headers
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported provider: {self.provider}")
|
||||||
|
|
||||||
|
async def _complete_minimax(
|
||||||
|
self,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: Optional[int],
|
||||||
|
headers: dict,
|
||||||
|
) -> str:
|
||||||
|
"""Call MiniMax API."""
|
||||||
|
# MiniMax uses chat/completions format
|
||||||
|
payload = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt},
|
||||||
|
],
|
||||||
|
"temperature": temperature,
|
||||||
|
}
|
||||||
|
|
||||||
|
if max_tokens:
|
||||||
|
payload["max_tokens"] = max_tokens
|
||||||
|
|
||||||
|
response = await self.client.post(
|
||||||
|
f"{self.base_url}/text/chatcompletion_v2",
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
return data["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
async def _complete_openai(
|
||||||
|
self,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
temperature: float,
|
||||||
|
max_tokens: Optional[int],
|
||||||
|
headers: dict,
|
||||||
|
) -> str:
|
||||||
|
"""Call OpenAI API."""
|
||||||
|
payload = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt},
|
||||||
|
],
|
||||||
|
"temperature": temperature,
|
||||||
|
}
|
||||||
|
|
||||||
|
if max_tokens:
|
||||||
|
payload["max_tokens"] = max_tokens
|
||||||
|
|
||||||
|
response = await self.client.post(
|
||||||
|
f"{self.base_url}/chat/completions",
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
return data["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Close the HTTP client."""
|
||||||
|
await self.client.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience function
|
||||||
|
def get_llm_client(config: Optional[Any] = None) -> LLMClient:
|
||||||
|
"""Get an LLM client from config."""
|
||||||
|
from opus_orchestrator.config import get_config
|
||||||
|
|
||||||
|
cfg = config or get_config()
|
||||||
|
|
||||||
|
return LLMClient(
|
||||||
|
api_key=cfg.agent.api_key,
|
||||||
|
provider=cfg.agent.provider,
|
||||||
|
model=cfg.agent.model,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user