Skip to content

Commit 4084f75

Browse files
authored
Merge pull request #137 from IBM/semantic_operators
Semantic operators
2 parents 2077435 + 570cda8 commit 4084f75

File tree

7 files changed

+94
-106
lines changed

7 files changed

+94
-106
lines changed

src/agentics/core/agentics.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
StateOperator,
5858
StateReducer,
5959
)
60-
from agentics.core.llm_connections import available_llms, get_llm_provider
60+
from agentics.core.llm_connections import get_cached_available_llms
6161
from agentics.core.utils import (
6262
chunk_list,
6363
get_function_io_types,
@@ -107,7 +107,8 @@ class AG(BaseModel, Generic[T]):
107107
"amap",
108108
description="Type of transduction to be used, amap, areduce",
109109
)
110-
llm: Any = Field(default_factory=get_llm_provider, exclude=True)
110+
# llm: Any = Field(default_factory=get_llm_provider, exclude=True)
111+
llm: Any = Field(default_factory=lambda: AG.get_llm_provider("first"), exclude=True)
111112

112113
provide_explanations: bool = False
113114
explanations: Optional[list[Explanation]] = None
@@ -218,6 +219,7 @@ class GeneratedAtype(BaseModel):
218219
def get_llm_provider(
219220
cls, provider_name: str = "first"
220221
) -> Union[LLM, dict[str, LLM]]:
222+
available_llms = get_cached_available_llms()
221223
if provider_name == "first":
222224
return (
223225
next(iter(available_llms.values()), None)

src/agentics/core/llm_connections.py

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
# Track which environment variables are used for each LLM
1111
_llms_env_vars: dict[str, list[str]] = {}
1212

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

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

5255

56+
def get_cached_available_llms() -> dict[str, LLM | AsyncOpenAI]:
57+
"""
58+
Get cached LLMs or compute and cache them on first call.
59+
60+
This avoids repeatedly scanning environment variables on every access.
61+
Call refresh_llm_cache() if you need to reload the configuration.
62+
"""
63+
global _available_llms_cache
64+
if _available_llms_cache is None:
65+
_available_llms_cache = get_available_llms()
66+
return _available_llms_cache
67+
68+
69+
def refresh_llm_cache() -> dict[str, LLM | AsyncOpenAI]:
70+
"""
71+
Force refresh the LLM cache.
72+
73+
Call this if environment variables change at runtime.
74+
"""
75+
global _available_llms_cache
76+
_available_llms_cache = None
77+
return get_cached_available_llms()
78+
79+
5380
def _get_llm_params(model: str) -> dict:
5481
"""
5582
Get provider-specific LLM parameters based on the model name.
@@ -108,23 +135,6 @@ def get_available_llms() -> dict[str, LLM | AsyncOpenAI]:
108135
)
109136
_llms_env_vars["ollama_llm"] = ["OLLAMA_MODEL_ID"]
110137

111-
# OpenAI LLM
112-
if _check_env("OPENAI_API_KEY"):
113-
openai_llm = LLM(
114-
model=os.getenv("OPENAI_MODEL_ID", "openai/gpt-4"),
115-
server_url=os.getenv("OPENAI_BASE_URL"),
116-
temperature=0.8,
117-
top_p=0.9,
118-
stop=["END"],
119-
api_key=os.getenv("OPENAI_API_KEY"),
120-
seed=42,
121-
)
122-
llms["openai_llm"] = openai_llm
123-
llms["openai"] = openai_llm
124-
env_vars = ["OPENAI_API_KEY", "OPENAI_MODEL_ID"]
125-
_llms_env_vars["openai_llm"] = env_vars
126-
_llms_env_vars["openai"] = env_vars
127-
128138
# OpenAI Compatible LLM
129139
if _check_env(
130140
"OPENAI_COMPATIBLE_API_KEY",
@@ -149,23 +159,6 @@ def get_available_llms() -> dict[str, LLM | AsyncOpenAI]:
149159
_llms_env_vars["openai_compatible_llm"] = env_vars
150160
_llms_env_vars["openai_compatible"] = env_vars
151161

152-
# WatsonX LLM
153-
if _check_env("WATSONX_APIKEY", "WATSONX_URL", "WATSONX_PROJECTID", "MODEL_ID"):
154-
watsonx_llm = LLM(
155-
model=os.getenv("MODEL_ID"),
156-
base_url=os.getenv("WATSONX_URL"),
157-
project_id=os.getenv("WATSONX_PROJECTID"),
158-
api_key=os.getenv("WATSONX_APIKEY"),
159-
temperature=0,
160-
max_tokens=4000,
161-
max_input_tokens=100000,
162-
)
163-
llms["watsonx_llm"] = watsonx_llm
164-
llms["watsonx"] = watsonx_llm
165-
env_vars = ["WATSONX_APIKEY", "WATSONX_URL", "WATSONX_PROJECTID", "MODEL_ID"]
166-
_llms_env_vars["watsonx_llm"] = env_vars
167-
_llms_env_vars["watsonx"] = env_vars
168-
169162
# VLLM (AsyncOpenAI)
170163
if _check_env("VLLM_URL"):
171164
llms["vllm_llm"] = AsyncOpenAI(
@@ -255,6 +248,23 @@ def get_available_llms() -> dict[str, LLM | AsyncOpenAI]:
255248
_llms_env_vars["litellm_proxy_llm"] = env_vars
256249
_llms_env_vars["litellm_proxy"] = env_vars
257250

251+
# OpenAI LLM
252+
if _check_env("OPENAI_API_KEY"):
253+
openai_llm = LLM(
254+
model=os.getenv("OPENAI_MODEL_ID", "openai/gpt-4"),
255+
base_url=os.getenv("OPENAI_BASE_URL"),
256+
temperature=0.8,
257+
top_p=0.9,
258+
stop=["END"],
259+
api_key=os.getenv("OPENAI_API_KEY"),
260+
seed=42,
261+
)
262+
llms["openai_llm"] = openai_llm
263+
llms["openai"] = openai_llm
264+
env_vars = ["OPENAI_API_KEY", "OPENAI_MODEL_ID"]
265+
_llms_env_vars["openai_llm"] = env_vars
266+
_llms_env_vars["openai"] = env_vars
267+
258268
return llms
259269

260270

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

270-
llms = get_available_llms()
281+
# llms = get_available_llms()
282+
llms = get_cached_available_llms()
271283
if name in llms:
272284
return llms[name]
273285

src/agentics/core/transducible_functions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ async def __call__(self, state: Any) -> Any: ...
587587
async def estimateLogicalProximity(func, llm=AG.get_llm_provider()):
588588
sources = await generate_prototypical_instances(func.input_model, llm=llm)
589589
targets = await func(sources)
590+
targets, explanations = _unpack_if_needed(targets)
590591
total_lp = 0
591592
if len(targets) > 0:
592593
for target, source in zip(targets, sources):

tutorials/logical_transduction_algebra.ipynb

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"from typing import Optional\n",
2929
"from agentics.core.atype import *\n",
3030
"from agentics.core.transducible_functions import With\n",
31+
"import pprint\n",
3132
"\n",
3233
"class GenericInput(BaseModel):\n",
3334
" content: Optional[str] = None\n",
@@ -63,12 +64,13 @@
6364
"metadata": {},
6465
"outputs": [],
6566
"source": [
67+
"from agentics.core.transducible_functions import _unpack_if_needed\n",
6668
"input = GenericInput(content=\n",
6769
" \"\"\"Zoran Mamdani become the NYC mayor\"\"\")\n",
6870
"\n",
6971
"write_tweet = Email << GenericInput\n",
70-
"tweet = await write_tweet(input)\n",
71-
"print(tweet.model_dump_json(indent=2))\n"
72+
"tweet, explanation = _unpack_if_needed(await write_tweet(input))\n",
73+
"pprint.pp(tweet.model_dump_json(indent=2))\n"
7274
]
7375
},
7476
{
@@ -112,10 +114,10 @@
112114
"write_mail_to_alfio = Email<< With(\n",
113115
" GenericInput,\n",
114116
" instructions=\"Write an email to Alfio Gliozzo\",\n",
115-
" prompyt_template=\"{content}\" )\n",
117+
" prompt_template=\"{content}\" )\n",
116118
"news = GenericInput(content=\"Zoran Mandani won the Election in NYC\")\n",
117-
"mail = await write_mail_to_alfio(news)\n",
118-
"print(mail)"
119+
"mail, explanations = _unpack_if_needed(await write_mail_to_alfio(news))\n",
120+
"pprint.pp(mail)"
119121
]
120122
},
121123
{
@@ -146,14 +148,13 @@
146148
"source": [
147149
"news = GenericInput(content=\"Zoran Mandani won the Election in NYC, make up a story about that.\")\n",
148150
"\n",
149-
"\n",
150151
"summary_composite_1 = Summary << write_mail_to_alfio\n",
151152
"summary = await summary_composite_1(news)\n",
152-
"print(summary.model_dump_json(indent=2))\n",
153+
"pprint.pp(summary.model_dump_json(indent=2))\n",
153154
"\n",
154155
"summary_composite_2 = Summary <<(Email<<GenericInput)\n",
155156
"mail = await summary_composite_2(news)\n",
156-
"print(mail.model_dump_json(indent=2))"
157+
"pprint.pp(mail.model_dump_json(indent=2))"
157158
]
158159
},
159160
{
@@ -196,8 +197,8 @@
196197
"outputs": [],
197198
"source": [
198199
"classify_genre= Genre << Movie\n",
199-
"genre = await classify_genre(movie)\n",
200-
"print((genre @ movie).model_dump_json(indent=2))"
200+
"genre, explanation = _unpack_if_needed(await classify_genre(movie))\n",
201+
"pprint.pp((genre @ movie).model_dump_json(indent=2))"
201202
]
202203
},
203204
{
@@ -220,8 +221,8 @@
220221
"classify_genre= Genre << With(Movie,\n",
221222
" provide_explanation=True)\n",
222223
"genre, explanation = await classify_genre(movie)\n",
223-
"print(genre.model_dump_json(indent=2))\n",
224-
"print(explanation.model_dump_json(indent=2))"
224+
"pprint.pp(genre.model_dump_json(indent=2))\n",
225+
"pprint.pp(explanation.model_dump_json(indent=2))"
225226
]
226227
}
227228
],

tutorials/map_reduce.ipynb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,8 @@
2929
"from typing import Optional\n",
3030
"from agentics import AG\n",
3131
"from pydantic import BaseModel, Field\n",
32-
"from dotenv import load_dotenv\n",
32+
"from agentics.core.transducible_functions import _unpack_if_needed\n",
3333
"\n",
34-
"# Load .env from project root (current working directory)\n",
35-
"load_dotenv()\n",
36-
"\n",
37-
"llm=AG.get_llm_provider(\"litellm_proxy\")\n",
3834
"\n",
3935
"class Number(BaseModel):\n",
4036
" number:Optional[int] = Field(None, description=\"An integer number\")\n",
@@ -147,7 +143,9 @@
147143
"to_number = Number << RomanNumber\n",
148144
"reduce = Number << With(Number, \n",
149145
" areduce=True,\n",
150-
" instructions=\"return the sum of the input numbers' number fields\")\n",
146+
" instructions=\"return the sum of the input numbers' number fields\",\n",
147+
" provide_explanation=False)\n",
148+
"\n",
151149
"\n",
152150
"await reduce(await to_number(roman_numbers ))"
153151
]

0 commit comments

Comments
 (0)