Skip to content
Merged
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
6 changes: 4 additions & 2 deletions src/agentics/core/agentics.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
StateOperator,
StateReducer,
)
from agentics.core.llm_connections import available_llms, get_llm_provider
from agentics.core.llm_connections import get_cached_available_llms
from agentics.core.utils import (
chunk_list,
get_function_io_types,
Expand Down Expand Up @@ -107,7 +107,8 @@ class AG(BaseModel, Generic[T]):
"amap",
description="Type of transduction to be used, amap, areduce",
)
llm: Any = Field(default_factory=get_llm_provider, exclude=True)
# llm: Any = Field(default_factory=get_llm_provider, exclude=True)
llm: Any = Field(default_factory=lambda: AG.get_llm_provider("first"), exclude=True)

provide_explanations: bool = False
explanations: Optional[list[Explanation]] = None
Expand Down Expand Up @@ -218,6 +219,7 @@ class GeneratedAtype(BaseModel):
def get_llm_provider(
cls, provider_name: str = "first"
) -> Union[LLM, dict[str, LLM]]:
available_llms = get_cached_available_llms()
if provider_name == "first":
return (
next(iter(available_llms.values()), None)
Expand Down
84 changes: 48 additions & 36 deletions src/agentics/core/llm_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
# Track which environment variables are used for each LLM
_llms_env_vars: dict[str, list[str]] = {}

# Cache for available LLMs (computed once at first use)
_available_llms_cache: dict[str, LLM | AsyncOpenAI] | None = None


def get_llm_provider(provider_name: str | None = None) -> LLM | AsyncOpenAI | None:
"""
Expand Down Expand Up @@ -50,6 +53,30 @@ def _check_env(*var_names: str) -> bool:
return all(os.getenv(var) for var in var_names)


def get_cached_available_llms() -> dict[str, LLM | AsyncOpenAI]:
"""
Get cached LLMs or compute and cache them on first call.

This avoids repeatedly scanning environment variables on every access.
Call refresh_llm_cache() if you need to reload the configuration.
"""
global _available_llms_cache
if _available_llms_cache is None:
_available_llms_cache = get_available_llms()
return _available_llms_cache


def refresh_llm_cache() -> dict[str, LLM | AsyncOpenAI]:
"""
Force refresh the LLM cache.

Call this if environment variables change at runtime.
"""
global _available_llms_cache
_available_llms_cache = None
return get_cached_available_llms()


def _get_llm_params(model: str) -> dict:
"""
Get provider-specific LLM parameters based on the model name.
Expand Down Expand Up @@ -108,23 +135,6 @@ def get_available_llms() -> dict[str, LLM | AsyncOpenAI]:
)
_llms_env_vars["ollama_llm"] = ["OLLAMA_MODEL_ID"]

# OpenAI LLM
if _check_env("OPENAI_API_KEY"):
openai_llm = LLM(
model=os.getenv("OPENAI_MODEL_ID", "openai/gpt-4"),
server_url=os.getenv("OPENAI_BASE_URL"),
temperature=0.8,
top_p=0.9,
stop=["END"],
api_key=os.getenv("OPENAI_API_KEY"),
seed=42,
)
llms["openai_llm"] = openai_llm
llms["openai"] = openai_llm
env_vars = ["OPENAI_API_KEY", "OPENAI_MODEL_ID"]
_llms_env_vars["openai_llm"] = env_vars
_llms_env_vars["openai"] = env_vars

# OpenAI Compatible LLM
if _check_env(
"OPENAI_COMPATIBLE_API_KEY",
Expand All @@ -149,23 +159,6 @@ def get_available_llms() -> dict[str, LLM | AsyncOpenAI]:
_llms_env_vars["openai_compatible_llm"] = env_vars
_llms_env_vars["openai_compatible"] = env_vars

# WatsonX LLM
if _check_env("WATSONX_APIKEY", "WATSONX_URL", "WATSONX_PROJECTID", "MODEL_ID"):
watsonx_llm = LLM(
model=os.getenv("MODEL_ID"),
base_url=os.getenv("WATSONX_URL"),
project_id=os.getenv("WATSONX_PROJECTID"),
api_key=os.getenv("WATSONX_APIKEY"),
temperature=0,
max_tokens=4000,
max_input_tokens=100000,
)
llms["watsonx_llm"] = watsonx_llm
llms["watsonx"] = watsonx_llm
env_vars = ["WATSONX_APIKEY", "WATSONX_URL", "WATSONX_PROJECTID", "MODEL_ID"]
_llms_env_vars["watsonx_llm"] = env_vars
_llms_env_vars["watsonx"] = env_vars

# VLLM (AsyncOpenAI)
if _check_env("VLLM_URL"):
llms["vllm_llm"] = AsyncOpenAI(
Expand Down Expand Up @@ -255,6 +248,23 @@ def get_available_llms() -> dict[str, LLM | AsyncOpenAI]:
_llms_env_vars["litellm_proxy_llm"] = env_vars
_llms_env_vars["litellm_proxy"] = env_vars

# OpenAI LLM
if _check_env("OPENAI_API_KEY"):
openai_llm = LLM(
model=os.getenv("OPENAI_MODEL_ID", "openai/gpt-4"),
base_url=os.getenv("OPENAI_BASE_URL"),
temperature=0.8,
top_p=0.9,
stop=["END"],
api_key=os.getenv("OPENAI_API_KEY"),
seed=42,
)
llms["openai_llm"] = openai_llm
llms["openai"] = openai_llm
env_vars = ["OPENAI_API_KEY", "OPENAI_MODEL_ID"]
_llms_env_vars["openai_llm"] = env_vars
_llms_env_vars["openai"] = env_vars

return llms


Expand All @@ -265,9 +275,11 @@ def __getattr__(name: str) -> dict[str, LLM | AsyncOpenAI] | LLM | AsyncOpenAI |
Allows accessing 'available_llms' and individual LLM variables dynamically.
"""
if name == "available_llms":
return get_available_llms()
# return get_available_llms()
return get_cached_available_llms()

llms = get_available_llms()
# llms = get_available_llms()
llms = get_cached_available_llms()
if name in llms:
return llms[name]

Expand Down
1 change: 1 addition & 0 deletions src/agentics/core/transducible_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ async def __call__(self, state: Any) -> Any: ...
async def estimateLogicalProximity(func, llm=AG.get_llm_provider()):
sources = await generate_prototypical_instances(func.input_model, llm=llm)
targets = await func(sources)
targets, explanations = _unpack_if_needed(targets)
total_lp = 0
if len(targets) > 0:
for target, source in zip(targets, sources):
Expand Down
25 changes: 13 additions & 12 deletions tutorials/logical_transduction_algebra.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"from typing import Optional\n",
"from agentics.core.atype import *\n",
"from agentics.core.transducible_functions import With\n",
"import pprint\n",
"\n",
"class GenericInput(BaseModel):\n",
" content: Optional[str] = None\n",
Expand Down Expand Up @@ -63,12 +64,13 @@
"metadata": {},
"outputs": [],
"source": [
"from agentics.core.transducible_functions import _unpack_if_needed\n",
"input = GenericInput(content=\n",
" \"\"\"Zoran Mamdani become the NYC mayor\"\"\")\n",
"\n",
"write_tweet = Email << GenericInput\n",
"tweet = await write_tweet(input)\n",
"print(tweet.model_dump_json(indent=2))\n"
"tweet, explanation = _unpack_if_needed(await write_tweet(input))\n",
"pprint.pp(tweet.model_dump_json(indent=2))\n"
]
},
{
Expand Down Expand Up @@ -112,10 +114,10 @@
"write_mail_to_alfio = Email<< With(\n",
" GenericInput,\n",
" instructions=\"Write an email to Alfio Gliozzo\",\n",
" prompyt_template=\"{content}\" )\n",
" prompt_template=\"{content}\" )\n",
"news = GenericInput(content=\"Zoran Mandani won the Election in NYC\")\n",
"mail = await write_mail_to_alfio(news)\n",
"print(mail)"
"mail, explanations = _unpack_if_needed(await write_mail_to_alfio(news))\n",
"pprint.pp(mail)"
]
},
{
Expand Down Expand Up @@ -146,14 +148,13 @@
"source": [
"news = GenericInput(content=\"Zoran Mandani won the Election in NYC, make up a story about that.\")\n",
"\n",
"\n",
"summary_composite_1 = Summary << write_mail_to_alfio\n",
"summary = await summary_composite_1(news)\n",
"print(summary.model_dump_json(indent=2))\n",
"pprint.pp(summary.model_dump_json(indent=2))\n",
"\n",
"summary_composite_2 = Summary <<(Email<<GenericInput)\n",
"mail = await summary_composite_2(news)\n",
"print(mail.model_dump_json(indent=2))"
"pprint.pp(mail.model_dump_json(indent=2))"
]
},
{
Expand Down Expand Up @@ -196,8 +197,8 @@
"outputs": [],
"source": [
"classify_genre= Genre << Movie\n",
"genre = await classify_genre(movie)\n",
"print((genre @ movie).model_dump_json(indent=2))"
"genre, explanation = _unpack_if_needed(await classify_genre(movie))\n",
"pprint.pp((genre @ movie).model_dump_json(indent=2))"
]
},
{
Expand All @@ -220,8 +221,8 @@
"classify_genre= Genre << With(Movie,\n",
" provide_explanation=True)\n",
"genre, explanation = await classify_genre(movie)\n",
"print(genre.model_dump_json(indent=2))\n",
"print(explanation.model_dump_json(indent=2))"
"pprint.pp(genre.model_dump_json(indent=2))\n",
"pprint.pp(explanation.model_dump_json(indent=2))"
]
}
],
Expand Down
10 changes: 4 additions & 6 deletions tutorials/map_reduce.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,8 @@
"from typing import Optional\n",
"from agentics import AG\n",
"from pydantic import BaseModel, Field\n",
"from dotenv import load_dotenv\n",
"from agentics.core.transducible_functions import _unpack_if_needed\n",
"\n",
"# Load .env from project root (current working directory)\n",
"load_dotenv()\n",
"\n",
"llm=AG.get_llm_provider(\"litellm_proxy\")\n",
"\n",
"class Number(BaseModel):\n",
" number:Optional[int] = Field(None, description=\"An integer number\")\n",
Expand Down Expand Up @@ -147,7 +143,9 @@
"to_number = Number << RomanNumber\n",
"reduce = Number << With(Number, \n",
" areduce=True,\n",
" instructions=\"return the sum of the input numbers' number fields\")\n",
" instructions=\"return the sum of the input numbers' number fields\",\n",
" provide_explanation=False)\n",
"\n",
"\n",
"await reduce(await to_number(roman_numbers ))"
]
Expand Down
Loading