aiagents4pharma 1.13.1__py3-none-any.whl → 1.14.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiagents4pharma/configs/config.yaml +2 -1
- aiagents4pharma/configs/talk2biomodels/__init__.py +1 -0
- aiagents4pharma/configs/talk2biomodels/agents/t2b_agent/default.yaml +2 -3
- aiagents4pharma/configs/talk2biomodels/tools/__init__.py +4 -0
- aiagents4pharma/configs/talk2biomodels/tools/ask_question/__init__.py +3 -0
- aiagents4pharma/talk2biomodels/__init__.py +1 -0
- aiagents4pharma/talk2biomodels/agents/t2b_agent.py +4 -2
- aiagents4pharma/talk2biomodels/api/__init__.py +6 -0
- aiagents4pharma/talk2biomodels/api/kegg.py +83 -0
- aiagents4pharma/talk2biomodels/api/ols.py +72 -0
- aiagents4pharma/talk2biomodels/api/uniprot.py +35 -0
- aiagents4pharma/talk2biomodels/states/state_talk2biomodels.py +21 -6
- aiagents4pharma/talk2biomodels/tests/test_api.py +57 -0
- aiagents4pharma/talk2biomodels/tests/test_ask_question.py +44 -0
- aiagents4pharma/talk2biomodels/tests/test_get_annotation.py +171 -0
- aiagents4pharma/talk2biomodels/tests/test_getmodelinfo.py +26 -0
- aiagents4pharma/talk2biomodels/tests/test_integration.py +126 -0
- aiagents4pharma/talk2biomodels/tests/test_param_scan.py +68 -0
- aiagents4pharma/talk2biomodels/tests/test_search_models.py +28 -0
- aiagents4pharma/talk2biomodels/tests/test_simulate_model.py +39 -0
- aiagents4pharma/talk2biomodels/tests/test_steady_state.py +90 -0
- aiagents4pharma/talk2biomodels/tools/__init__.py +1 -0
- aiagents4pharma/talk2biomodels/tools/ask_question.py +29 -8
- aiagents4pharma/talk2biomodels/tools/get_annotation.py +304 -0
- aiagents4pharma/talk2biomodels/tools/load_arguments.py +114 -0
- aiagents4pharma/talk2biomodels/tools/parameter_scan.py +91 -96
- aiagents4pharma/talk2biomodels/tools/simulate_model.py +14 -81
- aiagents4pharma/talk2biomodels/tools/steady_state.py +48 -89
- {aiagents4pharma-1.13.1.dist-info → aiagents4pharma-1.14.1.dist-info}/METADATA +1 -1
- {aiagents4pharma-1.13.1.dist-info → aiagents4pharma-1.14.1.dist-info}/RECORD +33 -17
- aiagents4pharma/talk2biomodels/tests/test_langgraph.py +0 -384
- {aiagents4pharma-1.13.1.dist-info → aiagents4pharma-1.14.1.dist-info}/LICENSE +0 -0
- {aiagents4pharma-1.13.1.dist-info → aiagents4pharma-1.14.1.dist-info}/WHEEL +0 -0
- {aiagents4pharma-1.13.1.dist-info → aiagents4pharma-1.14.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
'''
|
2
|
+
Test cases for Talk2Biomodels.
|
3
|
+
'''
|
4
|
+
|
5
|
+
import pandas as pd
|
6
|
+
from langchain_core.messages import HumanMessage, ToolMessage
|
7
|
+
from ..agents.t2b_agent import get_app
|
8
|
+
|
9
|
+
def test_integration():
|
10
|
+
'''
|
11
|
+
Test the integration of the tools.
|
12
|
+
'''
|
13
|
+
unique_id = 1234567
|
14
|
+
app = get_app(unique_id)
|
15
|
+
config = {"configurable": {"thread_id": unique_id}}
|
16
|
+
app.update_state(config, {"llm_model": "gpt-4o-mini"})
|
17
|
+
# ##########################################
|
18
|
+
# ## Test simulate_model tool
|
19
|
+
# ##########################################
|
20
|
+
prompt = '''Simulate the model 537 for 100 hours and intervals
|
21
|
+
100 with an initial concentration of `DoseQ2W`
|
22
|
+
set to 300 and `Dose` set to 0. Reset the concentration
|
23
|
+
of `Ab{serum}` to 100 every 25 hours.'''
|
24
|
+
# Test the tool get_modelinfo
|
25
|
+
response = app.invoke(
|
26
|
+
{"messages": [HumanMessage(content=prompt)]},
|
27
|
+
config=config
|
28
|
+
)
|
29
|
+
assistant_msg = response["messages"][-1].content
|
30
|
+
print (assistant_msg)
|
31
|
+
# Check if the assistant message is a string
|
32
|
+
assert isinstance(assistant_msg, str)
|
33
|
+
##########################################
|
34
|
+
# Test ask_question tool when simulation
|
35
|
+
# results are available
|
36
|
+
##########################################
|
37
|
+
# Update state
|
38
|
+
app.update_state(config, {"llm_model": "gpt-4o-mini"})
|
39
|
+
prompt = """What is the concentration of CRP in serum after 100 hours?
|
40
|
+
Round off the value to 2 decimal places."""
|
41
|
+
# Test the tool get_modelinfo
|
42
|
+
response = app.invoke(
|
43
|
+
{"messages": [HumanMessage(content=prompt)]},
|
44
|
+
config=config
|
45
|
+
)
|
46
|
+
assistant_msg = response["messages"][-1].content
|
47
|
+
# print (assistant_msg)
|
48
|
+
# Check if the assistant message is a string
|
49
|
+
assert '211' in assistant_msg
|
50
|
+
|
51
|
+
##########################################
|
52
|
+
# Test custom_plotter tool when the
|
53
|
+
# simulation results are available
|
54
|
+
##########################################
|
55
|
+
prompt = "Plot only CRP related species."
|
56
|
+
|
57
|
+
# Update state
|
58
|
+
app.update_state(config, {"llm_model": "gpt-4o-mini"}
|
59
|
+
)
|
60
|
+
# Test the tool get_modelinfo
|
61
|
+
response = app.invoke(
|
62
|
+
{"messages": [HumanMessage(content=prompt)]},
|
63
|
+
config=config
|
64
|
+
)
|
65
|
+
assistant_msg = response["messages"][-1].content
|
66
|
+
current_state = app.get_state(config)
|
67
|
+
# Get the messages from the current state
|
68
|
+
# and reverse the order
|
69
|
+
reversed_messages = current_state.values["messages"][::-1]
|
70
|
+
# Loop through the reversed messages
|
71
|
+
# until a ToolMessage is found.
|
72
|
+
expected_header = ['Time', 'CRP{serum}', 'CRPExtracellular']
|
73
|
+
expected_header += ['CRP Suppression (%)', 'CRP (% of baseline)']
|
74
|
+
expected_header += ['CRP{liver}']
|
75
|
+
predicted_artifact = []
|
76
|
+
for msg in reversed_messages:
|
77
|
+
if isinstance(msg, ToolMessage):
|
78
|
+
# Work on the message if it is a ToolMessage
|
79
|
+
# These may contain additional visuals that
|
80
|
+
# need to be displayed to the user.
|
81
|
+
if msg.name == "custom_plotter":
|
82
|
+
predicted_artifact = msg.artifact
|
83
|
+
break
|
84
|
+
# Convert the artifact into a pandas dataframe
|
85
|
+
# for easy comparison
|
86
|
+
df = pd.DataFrame(predicted_artifact)
|
87
|
+
# Extract the headers from the dataframe
|
88
|
+
predicted_header = df.columns.tolist()
|
89
|
+
# Check if the header is in the expected_header
|
90
|
+
# assert expected_header in predicted_artifact
|
91
|
+
assert set(expected_header).issubset(set(predicted_header))
|
92
|
+
##########################################
|
93
|
+
# Test custom_plotter tool when the
|
94
|
+
# simulation results are available but
|
95
|
+
# the species is not available
|
96
|
+
##########################################
|
97
|
+
prompt = """Make a custom plot showing the
|
98
|
+
concentration of the species `TP53` over
|
99
|
+
time. Do not show any other species."""
|
100
|
+
# Update state
|
101
|
+
app.update_state(config, {"llm_model": "gpt-4o-mini"}
|
102
|
+
)
|
103
|
+
# Test the tool get_modelinfo
|
104
|
+
response = app.invoke(
|
105
|
+
{"messages": [HumanMessage(content=prompt)]},
|
106
|
+
config=config
|
107
|
+
)
|
108
|
+
assistant_msg = response["messages"][-1].content
|
109
|
+
# print (response["messages"])
|
110
|
+
current_state = app.get_state(config)
|
111
|
+
# Get the messages from the current state
|
112
|
+
# and reverse the order
|
113
|
+
reversed_messages = current_state.values["messages"][::-1]
|
114
|
+
# Loop through the reversed messages until a
|
115
|
+
# ToolMessage is found.
|
116
|
+
predicted_artifact = []
|
117
|
+
for msg in reversed_messages:
|
118
|
+
if isinstance(msg, ToolMessage):
|
119
|
+
# Work on the message if it is a ToolMessage
|
120
|
+
# These may contain additional visuals that
|
121
|
+
# need to be displayed to the user.
|
122
|
+
if msg.name == "custom_plotter":
|
123
|
+
predicted_artifact = msg.artifact
|
124
|
+
break
|
125
|
+
# Check if the the predicted artifact is `None`
|
126
|
+
assert predicted_artifact is None
|
@@ -0,0 +1,68 @@
|
|
1
|
+
'''
|
2
|
+
Test cases for Talk2Biomodels parameter scan tool.
|
3
|
+
'''
|
4
|
+
|
5
|
+
import pandas as pd
|
6
|
+
from langchain_core.messages import HumanMessage, ToolMessage
|
7
|
+
from ..agents.t2b_agent import get_app
|
8
|
+
|
9
|
+
def test_param_scan_tool():
|
10
|
+
'''
|
11
|
+
In this test, we will test the parameter_scan tool.
|
12
|
+
We will prompt it to scan the parameter `kIL6RBind`
|
13
|
+
from 1 to 100 in steps of 10, record the changes
|
14
|
+
in the concentration of the species `Ab{serum}` in
|
15
|
+
model 537.
|
16
|
+
|
17
|
+
We will pass the inaccuarate parameter (`KIL6Rbind`)
|
18
|
+
and species names (just `Ab`) to the tool to test
|
19
|
+
if it can deal with it.
|
20
|
+
|
21
|
+
We expect the agent to first invoke the parameter_scan
|
22
|
+
tool and raise an error. It will then invoke another
|
23
|
+
tool get_modelinfo to get the correct parameter
|
24
|
+
and species names. Finally, the agent will reinvoke
|
25
|
+
the parameter_scan tool with the correct parameter
|
26
|
+
and species names.
|
27
|
+
|
28
|
+
'''
|
29
|
+
unique_id = 1234
|
30
|
+
app = get_app(unique_id)
|
31
|
+
config = {"configurable": {"thread_id": unique_id}}
|
32
|
+
app.update_state(config, {"llm_model": "gpt-4o-mini"})
|
33
|
+
prompt = """How will the value of Ab in serum in model 537 change
|
34
|
+
if the param kIL6Rbind is varied from 1 to 100 in steps of 10?
|
35
|
+
Set the initial `DoseQ2W` concentration to 300. Assume
|
36
|
+
that the model is simulated for 2016 hours with an interval of 50."""
|
37
|
+
# Invoke the agent
|
38
|
+
app.invoke(
|
39
|
+
{"messages": [HumanMessage(content=prompt)]},
|
40
|
+
config=config
|
41
|
+
)
|
42
|
+
current_state = app.get_state(config)
|
43
|
+
reversed_messages = current_state.values["messages"][::-1]
|
44
|
+
# Loop through the reversed messages until a
|
45
|
+
# ToolMessage is found.
|
46
|
+
df = pd.DataFrame(columns=['name', 'status', 'content'])
|
47
|
+
names = []
|
48
|
+
statuses = []
|
49
|
+
contents = []
|
50
|
+
for msg in reversed_messages:
|
51
|
+
# Assert that the message is a ToolMessage
|
52
|
+
# and its status is "error"
|
53
|
+
if not isinstance(msg, ToolMessage):
|
54
|
+
continue
|
55
|
+
names.append(msg.name)
|
56
|
+
statuses.append(msg.status)
|
57
|
+
contents.append(msg.content)
|
58
|
+
df = pd.DataFrame({'name': names, 'status': statuses, 'content': contents})
|
59
|
+
# print (df)
|
60
|
+
assert any((df["status"] == "error") &
|
61
|
+
(df["name"] == "parameter_scan") &
|
62
|
+
(df["content"].str.startswith(
|
63
|
+
"Error: ValueError('Invalid species or parameter name:")))
|
64
|
+
assert any((df["status"] == "success") &
|
65
|
+
(df["name"] == "parameter_scan") &
|
66
|
+
(df["content"].str.startswith("Parameter scan results of")))
|
67
|
+
assert any((df["status"] == "success") &
|
68
|
+
(df["name"] == "get_modelinfo"))
|
@@ -0,0 +1,28 @@
|
|
1
|
+
'''
|
2
|
+
Test cases for Talk2Biomodels search models tool.
|
3
|
+
'''
|
4
|
+
|
5
|
+
from langchain_core.messages import HumanMessage
|
6
|
+
from ..agents.t2b_agent import get_app
|
7
|
+
|
8
|
+
def test_search_models_tool():
|
9
|
+
'''
|
10
|
+
Test the search_models tool.
|
11
|
+
'''
|
12
|
+
unique_id = 12345
|
13
|
+
app = get_app(unique_id)
|
14
|
+
config = {"configurable": {"thread_id": unique_id}}
|
15
|
+
# Update state
|
16
|
+
app.update_state(config, {"llm_model": "gpt-4o-mini"})
|
17
|
+
prompt = "Search for models on Crohn's disease."
|
18
|
+
# Test the tool get_modelinfo
|
19
|
+
response = app.invoke(
|
20
|
+
{"messages": [HumanMessage(content=prompt)]},
|
21
|
+
config=config
|
22
|
+
)
|
23
|
+
assistant_msg = response["messages"][-1].content
|
24
|
+
# Check if the assistant message is a string
|
25
|
+
assert isinstance(assistant_msg, str)
|
26
|
+
# Check if the assistant message contains the
|
27
|
+
# biomodel id BIO0000000537
|
28
|
+
assert "BIOMD0000000537" in assistant_msg
|
@@ -0,0 +1,39 @@
|
|
1
|
+
'''
|
2
|
+
Test cases for Talk2Biomodels.
|
3
|
+
'''
|
4
|
+
|
5
|
+
from langchain_core.messages import HumanMessage
|
6
|
+
from ..agents.t2b_agent import get_app
|
7
|
+
|
8
|
+
def test_simulate_model_tool():
|
9
|
+
'''
|
10
|
+
Test the simulate_model tool when simulating
|
11
|
+
multiple models.
|
12
|
+
'''
|
13
|
+
unique_id = 123
|
14
|
+
app = get_app(unique_id)
|
15
|
+
config = {"configurable": {"thread_id": unique_id}}
|
16
|
+
app.update_state(config, {"llm_model": "gpt-4o-mini"})
|
17
|
+
# Upload a model to the state
|
18
|
+
app.update_state(config,
|
19
|
+
{"sbml_file_path": ["aiagents4pharma/talk2biomodels/tests/BIOMD0000000449_url.xml"]})
|
20
|
+
prompt = "Simulate model 64 and the uploaded model"
|
21
|
+
# Invoke the agent
|
22
|
+
app.invoke(
|
23
|
+
{"messages": [HumanMessage(content=prompt)]},
|
24
|
+
config=config
|
25
|
+
)
|
26
|
+
current_state = app.get_state(config)
|
27
|
+
dic_simulated_data = current_state.values["dic_simulated_data"]
|
28
|
+
# Check if the dic_simulated_data is a list
|
29
|
+
assert isinstance(dic_simulated_data, list)
|
30
|
+
# Check if the length of the dic_simulated_data is 2
|
31
|
+
assert len(dic_simulated_data) == 2
|
32
|
+
# Check if the source of the first model is 64
|
33
|
+
assert dic_simulated_data[0]['source'] == 64
|
34
|
+
# Check if the source of the second model is upload
|
35
|
+
assert dic_simulated_data[1]['source'] == "upload"
|
36
|
+
# Check if the data of the first model contains
|
37
|
+
assert '1,3-bisphosphoglycerate' in dic_simulated_data[0]['data']
|
38
|
+
# Check if the data of the second model contains
|
39
|
+
assert 'mTORC2' in dic_simulated_data[1]['data']
|
@@ -0,0 +1,90 @@
|
|
1
|
+
'''
|
2
|
+
Test cases for Talk2Biomodels steady state tool.
|
3
|
+
'''
|
4
|
+
|
5
|
+
from langchain_core.messages import HumanMessage, ToolMessage
|
6
|
+
from ..agents.t2b_agent import get_app
|
7
|
+
|
8
|
+
def test_steady_state_tool():
|
9
|
+
'''
|
10
|
+
Test the steady_state tool.
|
11
|
+
'''
|
12
|
+
unique_id = 123
|
13
|
+
app = get_app(unique_id)
|
14
|
+
config = {"configurable": {"thread_id": unique_id}}
|
15
|
+
app.update_state(config, {"llm_model": "gpt-4o-mini"})
|
16
|
+
#########################################################
|
17
|
+
# In this case, we will test if the tool returns an error
|
18
|
+
# when the model does not achieve a steady state. The tool
|
19
|
+
# status should be "error".
|
20
|
+
prompt = """Run a steady state analysis of model 537."""
|
21
|
+
# Invoke the agent
|
22
|
+
app.invoke(
|
23
|
+
{"messages": [HumanMessage(content=prompt)]},
|
24
|
+
config=config
|
25
|
+
)
|
26
|
+
current_state = app.get_state(config)
|
27
|
+
reversed_messages = current_state.values["messages"][::-1]
|
28
|
+
tool_msg_status = None
|
29
|
+
for msg in reversed_messages:
|
30
|
+
# Assert that the status of the
|
31
|
+
# ToolMessage is "error"
|
32
|
+
if isinstance(msg, ToolMessage):
|
33
|
+
# print (msg)
|
34
|
+
tool_msg_status = msg.status
|
35
|
+
break
|
36
|
+
assert tool_msg_status == "error"
|
37
|
+
#########################################################
|
38
|
+
# In this case, we will test if the tool is indeed invoked
|
39
|
+
# successfully
|
40
|
+
prompt = """Run a steady state analysis of model 64.
|
41
|
+
Set the initial concentration of `Pyruvate` to 0.2. The
|
42
|
+
concentration of `NAD` resets to 100 every 2 time units."""
|
43
|
+
# Invoke the agent
|
44
|
+
app.invoke(
|
45
|
+
{"messages": [HumanMessage(content=prompt)]},
|
46
|
+
config=config
|
47
|
+
)
|
48
|
+
# Loop through the reversed messages until a
|
49
|
+
# ToolMessage is found.
|
50
|
+
current_state = app.get_state(config)
|
51
|
+
reversed_messages = current_state.values["messages"][::-1]
|
52
|
+
steady_state_invoked = False
|
53
|
+
for msg in reversed_messages:
|
54
|
+
# Assert that the message is a ToolMessage
|
55
|
+
# and its status is "error"
|
56
|
+
if isinstance(msg, ToolMessage):
|
57
|
+
print (msg)
|
58
|
+
if msg.name == "steady_state" and msg.status != "error":
|
59
|
+
steady_state_invoked = True
|
60
|
+
break
|
61
|
+
assert steady_state_invoked
|
62
|
+
#########################################################
|
63
|
+
# In this case, we will test if the `ask_question` tool is
|
64
|
+
# invoked upon asking a question about the already generated
|
65
|
+
# steady state results
|
66
|
+
prompt = """What is the Phosphoenolpyruvate concentration
|
67
|
+
at the steady state? Show only the concentration, rounded
|
68
|
+
to 2 decimal places. For example, if the concentration is
|
69
|
+
0.123456, your response should be `0.12`. Do not return
|
70
|
+
any other information."""
|
71
|
+
# Invoke the agent
|
72
|
+
response = app.invoke(
|
73
|
+
{"messages": [HumanMessage(content=prompt)]},
|
74
|
+
config=config
|
75
|
+
)
|
76
|
+
assistant_msg = response["messages"][-1].content
|
77
|
+
current_state = app.get_state(config)
|
78
|
+
reversed_messages = current_state.values["messages"][::-1]
|
79
|
+
# Loop through the reversed messages until a
|
80
|
+
# ToolMessage is found.
|
81
|
+
ask_questool_invoked = False
|
82
|
+
for msg in reversed_messages:
|
83
|
+
# Assert that the message is a ToolMessage
|
84
|
+
# and its status is "error"
|
85
|
+
if isinstance(msg, ToolMessage):
|
86
|
+
if msg.name == "ask_question":
|
87
|
+
ask_questool_invoked = True
|
88
|
+
break
|
89
|
+
assert ask_questool_invoked
|
90
|
+
assert "0.06" in assistant_msg
|
@@ -6,10 +6,11 @@ Tool for asking a question about the simulation results.
|
|
6
6
|
|
7
7
|
import logging
|
8
8
|
from typing import Type, Annotated, Literal
|
9
|
+
import hydra
|
10
|
+
import basico
|
9
11
|
import pandas as pd
|
10
12
|
from pydantic import BaseModel, Field
|
11
13
|
from langchain_core.tools.base import BaseTool
|
12
|
-
from langchain.agents.agent_types import AgentType
|
13
14
|
from langchain_experimental.agents import create_pandas_dataframe_agent
|
14
15
|
from langchain_openai import ChatOpenAI
|
15
16
|
from langgraph.prebuilt import InjectedState
|
@@ -64,31 +65,51 @@ class AskQuestionTool(BaseTool):
|
|
64
65
|
question,
|
65
66
|
question_context,
|
66
67
|
experiment_name)
|
67
|
-
#
|
68
|
+
# Load hydra configuration
|
69
|
+
with hydra.initialize(version_base=None, config_path="../../configs"):
|
70
|
+
cfg = hydra.compose(config_name='config',
|
71
|
+
overrides=['talk2biomodels/tools/ask_question=default'])
|
72
|
+
cfg = cfg.talk2biomodels.tools.ask_question
|
73
|
+
# Get the context of the question
|
74
|
+
# and based on the context, get the data
|
75
|
+
# and prompt content to ask the question
|
68
76
|
if question_context == "steady_state":
|
69
77
|
dic_context = state["dic_steady_state_data"]
|
78
|
+
prompt_content = cfg.steady_state_prompt
|
70
79
|
else:
|
71
80
|
dic_context = state["dic_simulated_data"]
|
81
|
+
prompt_content = cfg.simulation_prompt
|
82
|
+
# Extract the
|
72
83
|
dic_data = {}
|
73
84
|
for data in dic_context:
|
74
85
|
for key in data:
|
75
86
|
if key not in dic_data:
|
76
87
|
dic_data[key] = []
|
77
88
|
dic_data[key] += [data[key]]
|
78
|
-
#
|
89
|
+
# Create a pandas dataframe of the data
|
79
90
|
df_data = pd.DataFrame.from_dict(dic_data)
|
91
|
+
# Extract the data for the experiment
|
92
|
+
# matching the experiment name
|
80
93
|
df = pd.DataFrame(
|
81
94
|
df_data[df_data['name'] == experiment_name]['data'].iloc[0]
|
82
95
|
)
|
83
|
-
|
84
|
-
#
|
85
|
-
#
|
86
|
-
#
|
96
|
+
logger.log(logging.INFO, "Shape of the dataframe: %s", df.shape)
|
97
|
+
# # Extract the model units
|
98
|
+
# model_units = basico.model_info.get_model_units()
|
99
|
+
# Update the prompt content with the model units
|
100
|
+
prompt_content += "Following are the model units:\n"
|
101
|
+
prompt_content += f"{basico.model_info.get_model_units()}\n\n"
|
102
|
+
# Create a pandas dataframe agent
|
87
103
|
df_agent = create_pandas_dataframe_agent(
|
88
104
|
ChatOpenAI(model=state['llm_model']),
|
89
105
|
allow_dangerous_code=True,
|
90
|
-
agent_type=
|
106
|
+
agent_type='tool-calling',
|
91
107
|
df=df,
|
108
|
+
max_iterations=5,
|
109
|
+
include_df_in_prompt=True,
|
110
|
+
number_of_head_rows=df.shape[0],
|
111
|
+
verbose=True,
|
92
112
|
prefix=prompt_content)
|
113
|
+
# Invoke the agent with the question
|
93
114
|
llm_result = df_agent.invoke(question)
|
94
115
|
return llm_result["output"]
|