bioguider 0.2.3__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.
Potentially problematic release.
This version of bioguider might be problematic. Click here for more details.
- bioguider/__init__.py +0 -0
- bioguider/agents/__init__.py +0 -0
- bioguider/agents/agent_task.py +88 -0
- bioguider/agents/agent_tools.py +147 -0
- bioguider/agents/agent_utils.py +357 -0
- bioguider/agents/collection_execute_step.py +180 -0
- bioguider/agents/collection_observe_step.py +113 -0
- bioguider/agents/collection_plan_step.py +154 -0
- bioguider/agents/collection_task.py +179 -0
- bioguider/agents/collection_task_utils.py +109 -0
- bioguider/agents/common_agent.py +159 -0
- bioguider/agents/common_agent_2step.py +126 -0
- bioguider/agents/common_step.py +85 -0
- bioguider/agents/dockergeneration_execute_step.py +186 -0
- bioguider/agents/dockergeneration_observe_step.py +153 -0
- bioguider/agents/dockergeneration_plan_step.py +158 -0
- bioguider/agents/dockergeneration_task.py +158 -0
- bioguider/agents/dockergeneration_task_utils.py +220 -0
- bioguider/agents/evaluation_task.py +269 -0
- bioguider/agents/identification_execute_step.py +179 -0
- bioguider/agents/identification_observe_step.py +92 -0
- bioguider/agents/identification_plan_step.py +135 -0
- bioguider/agents/identification_task.py +220 -0
- bioguider/agents/identification_task_utils.py +18 -0
- bioguider/agents/peo_common_step.py +64 -0
- bioguider/agents/prompt_utils.py +190 -0
- bioguider/agents/python_ast_repl_tool.py +69 -0
- bioguider/agents/rag_collection_task.py +130 -0
- bioguider/conversation.py +67 -0
- bioguider/database/summarized_file_db.py +140 -0
- bioguider/managers/evaluation_manager.py +108 -0
- bioguider/rag/__init__.py +0 -0
- bioguider/rag/config.py +117 -0
- bioguider/rag/data_pipeline.py +648 -0
- bioguider/rag/embedder.py +24 -0
- bioguider/rag/rag.py +134 -0
- bioguider/settings.py +103 -0
- bioguider/utils/constants.py +40 -0
- bioguider/utils/default.gitignore +140 -0
- bioguider/utils/file_utils.py +126 -0
- bioguider/utils/gitignore_checker.py +175 -0
- bioguider/utils/pyphen_utils.py +73 -0
- bioguider/utils/utils.py +27 -0
- bioguider-0.2.3.dist-info/LICENSE +21 -0
- bioguider-0.2.3.dist-info/METADATA +44 -0
- bioguider-0.2.3.dist-info/RECORD +47 -0
- bioguider-0.2.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Callable, Optional, TypedDict
|
|
3
|
+
from langchain.prompts import ChatPromptTemplate
|
|
4
|
+
from langchain_openai.chat_models.base import BaseChatOpenAI
|
|
5
|
+
from langchain_core.messages import AIMessage
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from bioguider.agents.agent_tools import agent_tool
|
|
9
|
+
from bioguider.agents.agent_utils import read_file, summarize_file
|
|
10
|
+
from bioguider.agents.peo_common_step import PEOWorkflowState
|
|
11
|
+
from bioguider.agents.common_agent import CommonAgent
|
|
12
|
+
from bioguider.agents.common_agent_2step import CommonAgentTwoSteps
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CollectionWorkflowState(TypedDict):
|
|
16
|
+
llm: Optional[BaseChatOpenAI]
|
|
17
|
+
step_output_callback: Optional[Callable]
|
|
18
|
+
|
|
19
|
+
intermediate_steps: Optional[str]
|
|
20
|
+
step_output: Optional[str]
|
|
21
|
+
step_analysis: Optional[str]
|
|
22
|
+
step_thoughts: Optional[str]
|
|
23
|
+
plan_actions: Optional[list[dict]]
|
|
24
|
+
|
|
25
|
+
goal_item: Optional[str]
|
|
26
|
+
final_answer: Optional[str]
|
|
27
|
+
|
|
28
|
+
RELATED_FILE_GOAL_ITEM = """
|
|
29
|
+
Your task is to determine whether the file is related to **{goal_item}**.
|
|
30
|
+
|
|
31
|
+
{related_file_description}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
CHECK_FILE_RELATED_USER_PROMPT = ChatPromptTemplate.from_template("""
|
|
35
|
+
You are given a summary of a file’s content.
|
|
36
|
+
|
|
37
|
+
{goal_item_desc}
|
|
38
|
+
|
|
39
|
+
Here is the file summary:
|
|
40
|
+
```
|
|
41
|
+
{summarized_file_content}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### **Question:**
|
|
45
|
+
Does this file appear to contain related information?
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### **Output Format:**
|
|
50
|
+
Respond with a single word: "Yes" or "No" to indicate whether the file is related to the goal item.
|
|
51
|
+
Do not include any additional text, explanation, or formatting.
|
|
52
|
+
""")
|
|
53
|
+
|
|
54
|
+
class CheckFileRelatedResult(BaseModel):
|
|
55
|
+
is_related: bool = Field(description="True if the file is related to the goal item, False otherwise.")
|
|
56
|
+
|
|
57
|
+
class check_file_related_tool(agent_tool):
|
|
58
|
+
""" Check if the file is related to the goal item
|
|
59
|
+
Args:
|
|
60
|
+
file_path str: file path
|
|
61
|
+
Returns:
|
|
62
|
+
bool: True if the file is related to the goal item, False otherwise.
|
|
63
|
+
"""
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
llm: BaseChatOpenAI,
|
|
67
|
+
repo_path: str,
|
|
68
|
+
goal_item_desc: str,
|
|
69
|
+
output_callback: Callable | None = None,
|
|
70
|
+
):
|
|
71
|
+
super().__init__(llm=llm, output_callback=output_callback)
|
|
72
|
+
self.repo_path = repo_path
|
|
73
|
+
self.goal_item_desc = goal_item_desc
|
|
74
|
+
|
|
75
|
+
def run(self, file_path: str) -> str:
|
|
76
|
+
if not self.repo_path in file_path:
|
|
77
|
+
file_path = os.path.join(self.repo_path, file_path)
|
|
78
|
+
if not os.path.isfile(file_path):
|
|
79
|
+
return "Can't read file"
|
|
80
|
+
file_content = read_file(file_path)
|
|
81
|
+
if file_content is None:
|
|
82
|
+
return "Failed to read file"
|
|
83
|
+
summarized_content, token_usage = summarize_file(self.llm, file_path, file_content, 6)
|
|
84
|
+
if summarized_content is None:
|
|
85
|
+
return "Failed to summarize file"
|
|
86
|
+
self._print_token_usage(token_usage)
|
|
87
|
+
|
|
88
|
+
prompt = CHECK_FILE_RELATED_USER_PROMPT.format(
|
|
89
|
+
goal_item_desc=self.goal_item_desc,
|
|
90
|
+
summarized_file_content=summarized_content,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
agent = CommonAgentTwoSteps(llm=self.llm)
|
|
94
|
+
res, _, token_usage, reasoning = agent.go(
|
|
95
|
+
system_prompt=prompt,
|
|
96
|
+
instruction_prompt="Now, please check if the file is related to the goal item.",
|
|
97
|
+
schema=CheckFileRelatedResult,
|
|
98
|
+
)
|
|
99
|
+
# res: AIMessage = self.llm.invoke([("human", prompt)])
|
|
100
|
+
res: CheckFileRelatedResult = res
|
|
101
|
+
out = res.is_related
|
|
102
|
+
|
|
103
|
+
self._print_step_output(step_output=reasoning)
|
|
104
|
+
self._print_token_usage(token_usage)
|
|
105
|
+
if out:
|
|
106
|
+
return "Yes, the file is related to the goal item."
|
|
107
|
+
else:
|
|
108
|
+
return "No, the file **is not** related to the goal item."
|
|
109
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import Any, Callable, Optional
|
|
2
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
3
|
+
from langchain_openai.chat_models.base import BaseChatOpenAI
|
|
4
|
+
from langchain_community.callbacks.openai_info import OpenAICallbackHandler
|
|
5
|
+
from langchain_core.messages import SystemMessage, HumanMessage
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from tenacity import retry, stop_after_attempt, wait_incrementing
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from bioguider.agents.agent_utils import (
|
|
11
|
+
escape_braces,
|
|
12
|
+
increase_token_usage,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class RetryException(Exception):
|
|
18
|
+
"""Exception need to retry"""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CommonAgentResult(BaseModel):
|
|
24
|
+
reasoning_process: str = Field(
|
|
25
|
+
description="A detailed explanation of the thought process or reasoning steps taken to reach a conclusion."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommonAgent:
|
|
30
|
+
def __init__(self, llm: BaseChatOpenAI):
|
|
31
|
+
self.llm = llm
|
|
32
|
+
self.exception: RetryException | None = None
|
|
33
|
+
self.token_usage: dict | None = None
|
|
34
|
+
|
|
35
|
+
def go(
|
|
36
|
+
self,
|
|
37
|
+
system_prompt: str,
|
|
38
|
+
instruction_prompt: str,
|
|
39
|
+
schema: any,
|
|
40
|
+
pre_process: Optional[Callable] = None,
|
|
41
|
+
post_process: Optional[Callable] = None,
|
|
42
|
+
**kwargs: Optional[Any],
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
execute agent
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
system_prompt str: system prompt
|
|
49
|
+
instruction_prompt str: user prompt to guide how llm execute agent
|
|
50
|
+
schema pydantic.BaseModel or json schema: llm output result schema
|
|
51
|
+
pre_process Callable or None: pre-processor that would be executed before llm.invoke
|
|
52
|
+
post_process Callable or None: post-processor that would be executed after llm.invoke
|
|
53
|
+
kwargs None or dict: args for pre_proces and post_process
|
|
54
|
+
|
|
55
|
+
Return:
|
|
56
|
+
(output that comply with input args `schema`)
|
|
57
|
+
"""
|
|
58
|
+
self._initialize()
|
|
59
|
+
if pre_process is not None:
|
|
60
|
+
is_OK = pre_process(**kwargs)
|
|
61
|
+
if not is_OK: # skip
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
return self._invoke_agent(
|
|
65
|
+
system_prompt,
|
|
66
|
+
instruction_prompt,
|
|
67
|
+
schema,
|
|
68
|
+
post_process,
|
|
69
|
+
**kwargs,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _initialize(self):
|
|
73
|
+
self.exception = None
|
|
74
|
+
self.token_usage = None
|
|
75
|
+
|
|
76
|
+
def _process_retryexception_message(
|
|
77
|
+
self, prompt: ChatPromptTemplate
|
|
78
|
+
) -> ChatPromptTemplate:
|
|
79
|
+
if self.exception is None:
|
|
80
|
+
return prompt
|
|
81
|
+
|
|
82
|
+
existing_messages = prompt.messages
|
|
83
|
+
updated_messages = existing_messages + [("human", str(self.exception))]
|
|
84
|
+
self.exception = None
|
|
85
|
+
updated_prompt = ChatPromptTemplate.from_messages(updated_messages)
|
|
86
|
+
return updated_prompt
|
|
87
|
+
|
|
88
|
+
def _incre_token_usage(self, token_usage):
|
|
89
|
+
incremental_token_usage = token_usage
|
|
90
|
+
if not isinstance(token_usage, dict):
|
|
91
|
+
incremental_token_usage = vars(incremental_token_usage)
|
|
92
|
+
self.token_usage = increase_token_usage(
|
|
93
|
+
self.token_usage, incremental_token_usage
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@retry(
|
|
97
|
+
stop=stop_after_attempt(5),
|
|
98
|
+
wait=wait_incrementing(start=1.0, increment=3, max=10),
|
|
99
|
+
)
|
|
100
|
+
def _invoke_agent(
|
|
101
|
+
self,
|
|
102
|
+
system_prompt: str,
|
|
103
|
+
instruction_prompt: str,
|
|
104
|
+
schema: any,
|
|
105
|
+
post_process: Optional[Callable] = None,
|
|
106
|
+
**kwargs: Optional[Any],
|
|
107
|
+
) -> tuple[Any, Any, dict | None, Any | None]:
|
|
108
|
+
system_prompt = escape_braces(system_prompt)
|
|
109
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
110
|
+
("system", system_prompt),
|
|
111
|
+
("human", instruction_prompt),
|
|
112
|
+
])
|
|
113
|
+
# Initialize the callback handler
|
|
114
|
+
callback_handler = OpenAICallbackHandler()
|
|
115
|
+
|
|
116
|
+
updated_prompt = self._process_retryexception_message(prompt)
|
|
117
|
+
agent = updated_prompt | self.llm.with_structured_output(schema)
|
|
118
|
+
try:
|
|
119
|
+
res = agent.invoke(
|
|
120
|
+
input={},
|
|
121
|
+
config={
|
|
122
|
+
"callbacks": [callback_handler],
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
self._incre_token_usage(callback_handler)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(str(e))
|
|
128
|
+
raise e
|
|
129
|
+
processed_res = res
|
|
130
|
+
if post_process is not None:
|
|
131
|
+
try:
|
|
132
|
+
processed_res = post_process(res, **kwargs)
|
|
133
|
+
except RetryException as e:
|
|
134
|
+
logger.error(str(e))
|
|
135
|
+
self.exception = e
|
|
136
|
+
raise e
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(str(e))
|
|
139
|
+
raise e
|
|
140
|
+
return res, processed_res, self.token_usage, None
|
|
141
|
+
|
|
142
|
+
class CommonConversation:
|
|
143
|
+
def __init__(self, llm: BaseChatOpenAI):
|
|
144
|
+
self.llm = llm
|
|
145
|
+
|
|
146
|
+
def generate(self, system_prompt: str, instruction_prompt: str):
|
|
147
|
+
msgs = [
|
|
148
|
+
SystemMessage(system_prompt),
|
|
149
|
+
HumanMessage(instruction_prompt),
|
|
150
|
+
]
|
|
151
|
+
msgs_template = ChatPromptTemplate.from_messages(messages=msgs)
|
|
152
|
+
callback_handler = OpenAICallbackHandler()
|
|
153
|
+
result = self.llm.generate(
|
|
154
|
+
messages=[msgs],
|
|
155
|
+
callbacks=[callback_handler]
|
|
156
|
+
)
|
|
157
|
+
response = result.generations[0][0].text
|
|
158
|
+
token_usage = result.llm_output.get("token_usage")
|
|
159
|
+
return response, token_usage
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from typing import Any, Callable, Optional
|
|
2
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
3
|
+
from langchain_openai.chat_models.base import BaseChatOpenAI
|
|
4
|
+
from langchain_community.callbacks.openai_info import OpenAICallbackHandler
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from tenacity import retry, stop_after_attempt, wait_incrementing
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from bioguider.agents.agent_utils import escape_braces
|
|
10
|
+
from bioguider.agents.common_agent import (
|
|
11
|
+
CommonAgent,
|
|
12
|
+
RetryException,
|
|
13
|
+
)
|
|
14
|
+
from bioguider.agents.prompt_utils import COT_USER_INSTRUCTION
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CommonAgentTwoSteps(CommonAgent):
|
|
20
|
+
def __init__(self, llm: BaseChatOpenAI):
|
|
21
|
+
super().__init__(llm)
|
|
22
|
+
|
|
23
|
+
def _initialize(self):
|
|
24
|
+
self.exceptions = None
|
|
25
|
+
self.token_usage = None
|
|
26
|
+
|
|
27
|
+
def _get_retryexception_message(self) -> list[tuple[str, str]]:
|
|
28
|
+
if self.exceptions is None:
|
|
29
|
+
return None
|
|
30
|
+
return [("human", str(excp)) for excp in self.exceptions]
|
|
31
|
+
|
|
32
|
+
def _build_prompt_for_cot_step(
|
|
33
|
+
self,
|
|
34
|
+
system_prompt: str,
|
|
35
|
+
instruction_prompt: str,
|
|
36
|
+
):
|
|
37
|
+
# system_prompt = system_prompt.replace("{", "{{").replace("}", "}}")
|
|
38
|
+
system_prompt = escape_braces(system_prompt)
|
|
39
|
+
instruction_prompt = instruction_prompt.replace("{", "{{").replace("}", "}}")
|
|
40
|
+
msgs = [("system", system_prompt)]
|
|
41
|
+
msgs = msgs + [("human", instruction_prompt)]
|
|
42
|
+
exception_msgs = self._get_retryexception_message()
|
|
43
|
+
if exception_msgs is not None:
|
|
44
|
+
msgs = msgs + exception_msgs
|
|
45
|
+
msgs = msgs + [("human", COT_USER_INSTRUCTION)]
|
|
46
|
+
return ChatPromptTemplate.from_messages(msgs)
|
|
47
|
+
|
|
48
|
+
def _build_prompt_for_final_step(
|
|
49
|
+
self,
|
|
50
|
+
system_prompt: str,
|
|
51
|
+
cot_msg: str,
|
|
52
|
+
):
|
|
53
|
+
system_prompt = system_prompt.replace("{", "{{").replace("}", "}}")
|
|
54
|
+
msgs = [("system", system_prompt)]
|
|
55
|
+
cot_msg = cot_msg.replace("{", "{{").replace("}", "}}")
|
|
56
|
+
msgs = msgs + [(
|
|
57
|
+
"human",
|
|
58
|
+
f"Please review the following step-by-step reasoning and provide the answer based on it: ```{cot_msg}```"
|
|
59
|
+
)]
|
|
60
|
+
return ChatPromptTemplate.from_messages(msgs)
|
|
61
|
+
|
|
62
|
+
@retry(
|
|
63
|
+
stop=stop_after_attempt(5),
|
|
64
|
+
wait=wait_incrementing(start=1.0, increment=3, max=10),
|
|
65
|
+
)
|
|
66
|
+
def _invoke_agent(
|
|
67
|
+
self,
|
|
68
|
+
system_prompt: str,
|
|
69
|
+
instruction_prompt: str,
|
|
70
|
+
schema: any,
|
|
71
|
+
post_process: Optional[Callable] = None,
|
|
72
|
+
**kwargs: Optional[Any],
|
|
73
|
+
):
|
|
74
|
+
# Initialize the callback handler
|
|
75
|
+
callback_handler = OpenAICallbackHandler()
|
|
76
|
+
cot_prompt = self._build_prompt_for_cot_step(
|
|
77
|
+
system_prompt=system_prompt,
|
|
78
|
+
instruction_prompt=instruction_prompt
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# First, use llm to do CoT
|
|
83
|
+
msgs = cot_prompt.invoke(input={}).to_messages()
|
|
84
|
+
|
|
85
|
+
cot_res = self.llm.generate(messages=[msgs])
|
|
86
|
+
reasoning_process = cot_res.generations[0][0].text
|
|
87
|
+
token_usage = cot_res.llm_output.get("token_usage")
|
|
88
|
+
cot_tokens = {
|
|
89
|
+
"total_tokens": token_usage.get("total_tokens", 0),
|
|
90
|
+
"prompt_tokens": token_usage.get("prompt_tokens", 0),
|
|
91
|
+
"completion_tokens": token_usage.get("completion_tokens", 0),
|
|
92
|
+
}
|
|
93
|
+
self._incre_token_usage(cot_tokens)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(str(e))
|
|
96
|
+
raise e
|
|
97
|
+
|
|
98
|
+
# Then use the reasoning process to do the structured output
|
|
99
|
+
updated_prompt = self._build_prompt_for_final_step(
|
|
100
|
+
system_prompt=system_prompt,
|
|
101
|
+
cot_msg=reasoning_process,
|
|
102
|
+
)
|
|
103
|
+
agent = updated_prompt | self.llm.with_structured_output(schema)
|
|
104
|
+
try:
|
|
105
|
+
res = agent.invoke(
|
|
106
|
+
input={},
|
|
107
|
+
config={
|
|
108
|
+
"callbacks": [callback_handler],
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
self._incre_token_usage(callback_handler)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(str(e))
|
|
114
|
+
raise e
|
|
115
|
+
processed_res = None
|
|
116
|
+
if post_process is not None:
|
|
117
|
+
try:
|
|
118
|
+
processed_res = post_process(res, **kwargs)
|
|
119
|
+
except RetryException as e:
|
|
120
|
+
logger.error(str(e))
|
|
121
|
+
self.exceptions = [e] if self.exceptions is None else self.exceptions + [e]
|
|
122
|
+
raise e
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(str(e))
|
|
125
|
+
raise e
|
|
126
|
+
return res, processed_res, self.token_usage, reasoning_process
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any, Callable, Dict, Optional, TypedDict
|
|
4
|
+
import logging
|
|
5
|
+
from langchain_openai.chat_models.base import BaseChatOpenAI
|
|
6
|
+
|
|
7
|
+
from bioguider.utils.constants import DEFAULT_TOKEN_USAGE
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
class CommonState(TypedDict):
|
|
12
|
+
"""
|
|
13
|
+
CommonState is a TypedDict that defines the structure of the state
|
|
14
|
+
used in the CommonStep class.
|
|
15
|
+
"""
|
|
16
|
+
llm: Optional[BaseChatOpenAI]
|
|
17
|
+
step_output_callback: Optional[Callable]
|
|
18
|
+
|
|
19
|
+
class CommonStep(ABC):
|
|
20
|
+
"""
|
|
21
|
+
CommonStep is a base class for defining common steps in a workflow.
|
|
22
|
+
It provides methods to execute the step and handle exceptions.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.step_name = ""
|
|
28
|
+
|
|
29
|
+
def enter_step(self, state):
|
|
30
|
+
if state["step_output_callback"] is None:
|
|
31
|
+
return
|
|
32
|
+
state["step_output_callback"](
|
|
33
|
+
step_name=self.step_name,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def leave_step(self, state, token_usage: Optional[dict[str, int]] = None):
|
|
37
|
+
if state["step_output_callback"] is None:
|
|
38
|
+
return
|
|
39
|
+
if token_usage is not None:
|
|
40
|
+
state["step_output_callback"](token_usage=token_usage)
|
|
41
|
+
|
|
42
|
+
def execute(self, state):
|
|
43
|
+
"""
|
|
44
|
+
Execute the step. This method should be overridden by subclasses.
|
|
45
|
+
"""
|
|
46
|
+
self.enter_step(state)
|
|
47
|
+
state, token_usage = self._execute_directly(state)
|
|
48
|
+
self.leave_step(state, token_usage)
|
|
49
|
+
return state
|
|
50
|
+
|
|
51
|
+
def _print_step(
|
|
52
|
+
self,
|
|
53
|
+
state,
|
|
54
|
+
step_name: str | None = None,
|
|
55
|
+
step_output: str | None = None,
|
|
56
|
+
token_usage: dict | object | None = None,
|
|
57
|
+
):
|
|
58
|
+
step_callback = state["step_output_callback"]
|
|
59
|
+
if step_callback is None:
|
|
60
|
+
return
|
|
61
|
+
# convert token_usage to dict
|
|
62
|
+
if token_usage is not None and not isinstance(token_usage, dict):
|
|
63
|
+
token_usage = vars(token_usage)
|
|
64
|
+
# In case token_usage.total_tokens is 0
|
|
65
|
+
token_usage = { **DEFAULT_TOKEN_USAGE, **token_usage }
|
|
66
|
+
step_callback(
|
|
67
|
+
step_name=step_name,
|
|
68
|
+
step_output=step_output,
|
|
69
|
+
token_usage=token_usage,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def _execute_directly(self, state) -> tuple[dict, dict[str, int]]:
|
|
74
|
+
"""
|
|
75
|
+
Execute the step directly. This method should be overridden by subclasses.
|
|
76
|
+
Args:
|
|
77
|
+
state (CommonState): The state of the workflow.
|
|
78
|
+
Returns:
|
|
79
|
+
tuple[dict, dict[str, int]]: The updated state and token usage.
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
|
|
2
|
+
import logging
|
|
3
|
+
from langchain_openai.chat_models.base import BaseChatOpenAI
|
|
4
|
+
from langchain.tools import BaseTool
|
|
5
|
+
from langchain.agents import create_react_agent, AgentExecutor
|
|
6
|
+
from langchain_community.callbacks.openai_info import OpenAICallbackHandler
|
|
7
|
+
|
|
8
|
+
from bioguider.utils.constants import DEFAULT_TOKEN_USAGE
|
|
9
|
+
from bioguider.agents.agent_utils import (
|
|
10
|
+
CustomPromptTemplate,
|
|
11
|
+
CustomOutputParser,
|
|
12
|
+
)
|
|
13
|
+
from bioguider.agents.peo_common_step import PEOCommonStep
|
|
14
|
+
from bioguider.agents.dockergeneration_task_utils import (
|
|
15
|
+
DockerGenerationWorkflowState,
|
|
16
|
+
generate_Dockerfile_tool,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
DOCKERGENERATION_EXECUTION_SYSTEM_PROMPT = """You are an expert in software containerization and reproducibility engineering.
|
|
22
|
+
You are given a **plan** and must complete it strictly using Python code and the available tools.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
### **Available Tools**
|
|
26
|
+
{tools}
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
### **Your Task**
|
|
30
|
+
Follow the given plan step by step using the exact format below:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Thought: Describe what you are thinking or planning to do next.
|
|
34
|
+
Action: The tool you are going to use (must be one of: {tool_names})
|
|
35
|
+
Action Input: The input to the selected action
|
|
36
|
+
Observation: The result returned by the action
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You may repeat the **Thought → Action → Action Input → Observation** loop as needed.
|
|
40
|
+
|
|
41
|
+
Once all steps in the plan have been executed, end the loop and output all the results and generated Dockerfile using this format:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Thought: I have completed the plan.
|
|
45
|
+
Final Answer:
|
|
46
|
+
Action: {{tool_name}}
|
|
47
|
+
Action Input: {{file_name1}}
|
|
48
|
+
Action Observation: {{Observation1}}
|
|
49
|
+
---
|
|
50
|
+
Action: {{tool_name}}
|
|
51
|
+
Action Input: {{file_name2}}
|
|
52
|
+
Action Observation: {{Observation2}}
|
|
53
|
+
---
|
|
54
|
+
**Dockerfile file name**: {{docker file path}}
|
|
55
|
+
...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### **Important Notes**
|
|
61
|
+
|
|
62
|
+
- You must strictly follow the provided plan.
|
|
63
|
+
- **Do not take any additional or alternative actions**, even if:
|
|
64
|
+
- No relevant result is found
|
|
65
|
+
- The file content is missing, empty, or irrelevant
|
|
66
|
+
- If no information is found in a step, simply proceed to the next action in the plan without improvising.
|
|
67
|
+
- Only use the tools specified in the plan actions. No independent decisions or extra steps are allowed.
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### **Plan**
|
|
71
|
+
{plan_actions}
|
|
72
|
+
|
|
73
|
+
### **Plan Thoughts**
|
|
74
|
+
{plan_thoughts}
|
|
75
|
+
|
|
76
|
+
### **Actions Already Taken**
|
|
77
|
+
{agent_scratchpad}
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
{input}
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
class DockerGenerationExecuteStep(PEOCommonStep):
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
llm: BaseChatOpenAI,
|
|
90
|
+
repo_path: str,
|
|
91
|
+
repo_structure: str,
|
|
92
|
+
gitignore_path: str,
|
|
93
|
+
custom_tools: list[BaseTool] | None = None,
|
|
94
|
+
):
|
|
95
|
+
super().__init__(llm)
|
|
96
|
+
self.step_name = "Docker Generation Execute Step"
|
|
97
|
+
self.repo_path = repo_path
|
|
98
|
+
self.repo_structure = repo_structure
|
|
99
|
+
self.gitignore_path = gitignore_path
|
|
100
|
+
self.custom_tools = custom_tools if custom_tools is not None else []
|
|
101
|
+
self.generate_tool: generate_Dockerfile_tool | None = None
|
|
102
|
+
|
|
103
|
+
def set_generate_Dockerfile_tool(self, tool: generate_Dockerfile_tool):
|
|
104
|
+
self.generate_tool = tool
|
|
105
|
+
|
|
106
|
+
def _execute_directly(self, state: DockerGenerationWorkflowState):
|
|
107
|
+
plan_actions = state["plan_actions"]
|
|
108
|
+
plan_thoughts = state["plan_thoughts"]
|
|
109
|
+
step_output = state["step_output"] if "step_output" in state and \
|
|
110
|
+
state["step_output"] is not None else "N/A"
|
|
111
|
+
step_dockerfile_content = state["step_dockerfile_content"] if "step_dockerfile_content" in state and \
|
|
112
|
+
state["step_dockerfile_content"] is not None else "N/A"
|
|
113
|
+
self.generate_tool.set_intermediate_output(
|
|
114
|
+
plan_thoughts=plan_thoughts,
|
|
115
|
+
step_error=step_output,
|
|
116
|
+
step_dockerfile_content=step_dockerfile_content,
|
|
117
|
+
)
|
|
118
|
+
prompt = CustomPromptTemplate(
|
|
119
|
+
template=DOCKERGENERATION_EXECUTION_SYSTEM_PROMPT,
|
|
120
|
+
tools=self.custom_tools,
|
|
121
|
+
plan_actions=plan_actions,
|
|
122
|
+
input_variables=[
|
|
123
|
+
"tools", "tool_names", "agent_scratchpad",
|
|
124
|
+
"intermediate_steps", "plan_actions", "plan_thoughts",
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
output_parser = CustomOutputParser()
|
|
128
|
+
agent = create_react_agent(
|
|
129
|
+
llm = self.llm,
|
|
130
|
+
tools = self.custom_tools,
|
|
131
|
+
prompt = prompt,
|
|
132
|
+
output_parser=output_parser,
|
|
133
|
+
stop_sequence=["\nObservation:"],
|
|
134
|
+
)
|
|
135
|
+
callback_handler = OpenAICallbackHandler()
|
|
136
|
+
agent_executor = AgentExecutor(
|
|
137
|
+
agent=agent,
|
|
138
|
+
tools=self.custom_tools,
|
|
139
|
+
max_iterations=10,
|
|
140
|
+
)
|
|
141
|
+
response = agent_executor.invoke(
|
|
142
|
+
input={
|
|
143
|
+
"plan_actions": plan_actions,
|
|
144
|
+
"plan_thoughts": plan_thoughts,
|
|
145
|
+
"input": "Now, let's begin."
|
|
146
|
+
},
|
|
147
|
+
config={
|
|
148
|
+
"callbacks": [callback_handler],
|
|
149
|
+
"recursion_limit": 20,
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
if "output" in response:
|
|
153
|
+
output = response["output"]
|
|
154
|
+
self._print_step(state, step_output=f"**Execute Output:** \n{output}")
|
|
155
|
+
if "**Final Answer**" in output:
|
|
156
|
+
final_answer = output.split("**Final Answer:**")[-1].strip().strip(":")
|
|
157
|
+
step_output = final_answer
|
|
158
|
+
elif "Final Answer" in output:
|
|
159
|
+
final_answer = output.split("Final Answer")[-1].strip().strip(":")
|
|
160
|
+
step_output = final_answer
|
|
161
|
+
else:
|
|
162
|
+
step_output = output
|
|
163
|
+
self._print_step(state, step_output=step_output)
|
|
164
|
+
state["step_output"] = step_output
|
|
165
|
+
if "**Dockerfile file name**" in step_output:
|
|
166
|
+
dockerfile: str = step_output.split("**Dockerfile file name**")[-1]
|
|
167
|
+
dockerfile = dockerfile.strip().strip(":")
|
|
168
|
+
dockerfile = dockerfile.strip("```").strip()
|
|
169
|
+
state["dockerfile"] = dockerfile
|
|
170
|
+
else:
|
|
171
|
+
state["dockerfile"] = None
|
|
172
|
+
# state["dockerfile"] = f"demo-bioguider-{docker_id}.Dockerfile"
|
|
173
|
+
else:
|
|
174
|
+
logger.error("No output found in the response.")
|
|
175
|
+
self._print_step(
|
|
176
|
+
state,
|
|
177
|
+
step_output="Error: No output found in the response.",
|
|
178
|
+
)
|
|
179
|
+
state["step_output"] = "Error: No output found in the response."
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
token_usage = vars(callback_handler)
|
|
183
|
+
token_usage = {**DEFAULT_TOKEN_USAGE, **token_usage}
|
|
184
|
+
|
|
185
|
+
return state, token_usage
|
|
186
|
+
|