diff --git a/src/agentics/core/agentics.py b/src/agentics/core/agentics.py index b8d07f310..87b6bc9bc 100644 --- a/src/agentics/core/agentics.py +++ b/src/agentics/core/agentics.py @@ -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, @@ -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 @@ -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) diff --git a/src/agentics/core/llm_connections.py b/src/agentics/core/llm_connections.py index dc973715b..00b1d5152 100644 --- a/src/agentics/core/llm_connections.py +++ b/src/agentics/core/llm_connections.py @@ -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: """ @@ -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. @@ -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", @@ -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( @@ -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 @@ -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] diff --git a/src/agentics/core/transducible_functions.py b/src/agentics/core/transducible_functions.py index 42b02e879..885805b7b 100644 --- a/src/agentics/core/transducible_functions.py +++ b/src/agentics/core/transducible_functions.py @@ -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): diff --git a/tutorials/logical_transduction_algebra.ipynb b/tutorials/logical_transduction_algebra.ipynb index 50022e5bd..43c190a14 100644 --- a/tutorials/logical_transduction_algebra.ipynb +++ b/tutorials/logical_transduction_algebra.ipynb @@ -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", @@ -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" ] }, { @@ -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)" ] }, { @@ -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< Email:\n", " \"\"\"Write an email about the provided content. Elaborate on that and make up content as needed\"\"\"\n", " return Transduce(state)\n", @@ -135,7 +128,7 @@ "source": [ "import re\n", "\n", - "@transducible(provide_explanation=False, llm=llm)\n", + "@transducible(provide_explanation=True)\n", "async def write_an_email_code_only(state: GenericInput) -> Email: \n", " match = re.match(r\"^(Hi|Dear|Hello|Hey)\\s+([^,]+),\\s*(.+)$\", state.content)\n", " if match:\n", @@ -143,7 +136,7 @@ " return Email(body= body, to=name, subject=\"\")\n", " else: return Email()\n", "\n", - "@transducible(provide_explanation=False, llm=llm)\n", + "@transducible(provide_explanation=True)\n", "async def write_an_email_to_lisa(state: GenericInput) -> Email:\n", " \"\"\"Write an email about the provided content. Elaborate on that and make up content as needed\"\"\"\n", " # example code to modify states before transduction\n", @@ -217,7 +210,7 @@ " relevant_sources:Optional[list[str]]=None\n", "\n", "\n", - "@transducible(tools=[web_search], llm=llm, reasoning=True, max_iter=20, provide_explanation=False)\n", + "@transducible(tools=[web_search], reasoning=True, max_iter=20, provide_explanation=False)\n", "async def answer_question_after_lookup(query: GenericInput) -> WebSearchResult:\n", " \"perform an extensive web search to provide an answer to the input question with supporting evidence. Use your tool to look it up\" \n", " return Transduce(query)\n", @@ -255,19 +248,20 @@ "from agentics.core.transducible_functions import Transduce, transducible\n", "\n", "\n", - "@transducible(provide_explanation=False, llm=llm, prompt_template=\"{movie_name}: {description}\")\n", + "@transducible(provide_explanation=False, prompt_template=\"{movie_name}: {description}\")\n", "async def classify_genre(state:Movie)-> Genre:\n", " \"\"\"Classify the genre of the source Movie \"\"\"\n", " return Transduce(state)\n", "\n", - "genre = await classify_genre(Movie(\n", + "from agentics.core.transducible_functions import _unpack_if_needed\n", + "\n", + "genre, explanation = _unpack_if_needed(await classify_genre(Movie(\n", " movie_name=\"The Godfather\",\n", " description=\"The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.\",\n", " year=1972\n", - "))\n", - "\n", + ")))\n", "print(genre.model_dump_json(indent=2))\n", - "print(explanation.model_dump_json(indent=2))\n" + "\n" ] } ],