diff --git a/core/src/agentforge_core/llm/azure.py b/core/src/agentforge_core/llm/azure.py new file mode 100644 index 0000000..0b1264c --- /dev/null +++ b/core/src/agentforge_core/llm/azure.py @@ -0,0 +1,78 @@ +"""Proveedor Azure OpenAI con retry exponencial y fallback configurable.""" + +from __future__ import annotations + +import asyncio +import time +from typing import Any + +from openai import AsyncAzureOpenAI + +from agentforge_core.llm.base import CompletionResult, Message + + +class AzureOpenAIProvider: + """Cliente Azure OpenAI con retry 3x backoff exponencial.""" + + name = "azure" + + def __init__( + self, + endpoint: str, + api_key: str, + deployment: str, + api_version: str, + ) -> None: + if not (endpoint and api_key and deployment): + raise ValueError("Azure OpenAI requiere endpoint, api_key y deployment.") + self._client = AsyncAzureOpenAI( + azure_endpoint=endpoint, + api_key=api_key, + api_version=api_version, + ) + self._deployment = deployment + + async def complete( + self, + messages: list[Message], + schema: dict[str, Any] | None = None, + temperature: float = 0.2, + max_tokens: int = 2000, + ) -> CompletionResult: + delays = [1.0, 2.0, 4.0] + last_exc: Exception | None = None + for attempt, delay in enumerate([0.0, *delays]): + if delay: + await asyncio.sleep(delay) + try: + return await self._call_once(messages, temperature, max_tokens) + except Exception as exc: + last_exc = exc + if attempt >= len(delays): + break + assert last_exc is not None + raise last_exc + + async def _call_once( + self, + messages: list[Message], + temperature: float, + max_tokens: int, + ) -> CompletionResult: + start = time.perf_counter() + resp = await self._client.chat.completions.create( # type: ignore[call-overload] + model=self._deployment, + messages=[m.model_dump() for m in messages], + temperature=temperature, + max_tokens=max_tokens, + response_format={"type": "json_object"}, + ) + elapsed = int((time.perf_counter() - start) * 1000) + choice = resp.choices[0] + return CompletionResult( + content=choice.message.content or "", + model=resp.model, + tokens_in=resp.usage.prompt_tokens if resp.usage else 0, + tokens_out=resp.usage.completion_tokens if resp.usage else 0, + latency_ms=elapsed, + ) diff --git a/core/src/agentforge_core/llm/openai.py b/core/src/agentforge_core/llm/openai.py new file mode 100644 index 0000000..457fbd4 --- /dev/null +++ b/core/src/agentforge_core/llm/openai.py @@ -0,0 +1,60 @@ +"""Proveedor OpenAI directo (fallback).""" + +from __future__ import annotations + +import asyncio +import time +from typing import Any + +from openai import AsyncOpenAI + +from agentforge_core.llm.base import CompletionResult, Message + + +class OpenAIProvider: + """Cliente OpenAI con misma polĂ­tica de retry que Azure.""" + + name = "openai" + + def __init__(self, api_key: str, model: str = "gpt-4o") -> None: + if not api_key: + raise ValueError("OpenAIProvider requiere api_key.") + self._client = AsyncOpenAI(api_key=api_key) + self._model = model + + async def complete( + self, + messages: list[Message], + schema: dict[str, Any] | None = None, + temperature: float = 0.2, + max_tokens: int = 2000, + ) -> CompletionResult: + delays = [1.0, 2.0, 4.0] + last_exc: Exception | None = None + for attempt, delay in enumerate([0.0, *delays]): + if delay: + await asyncio.sleep(delay) + try: + start = time.perf_counter() + resp = await self._client.chat.completions.create( # type: ignore[call-overload] + model=self._model, + messages=[m.model_dump() for m in messages], + temperature=temperature, + max_tokens=max_tokens, + response_format={"type": "json_object"}, + ) + elapsed = int((time.perf_counter() - start) * 1000) + choice = resp.choices[0] + return CompletionResult( + content=choice.message.content or "", + model=resp.model, + tokens_in=resp.usage.prompt_tokens if resp.usage else 0, + tokens_out=resp.usage.completion_tokens if resp.usage else 0, + latency_ms=elapsed, + ) + except Exception as exc: + last_exc = exc + if attempt >= len(delays): + break + assert last_exc is not None + raise last_exc