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.

Files changed (47) hide show
  1. bioguider/__init__.py +0 -0
  2. bioguider/agents/__init__.py +0 -0
  3. bioguider/agents/agent_task.py +88 -0
  4. bioguider/agents/agent_tools.py +147 -0
  5. bioguider/agents/agent_utils.py +357 -0
  6. bioguider/agents/collection_execute_step.py +180 -0
  7. bioguider/agents/collection_observe_step.py +113 -0
  8. bioguider/agents/collection_plan_step.py +154 -0
  9. bioguider/agents/collection_task.py +179 -0
  10. bioguider/agents/collection_task_utils.py +109 -0
  11. bioguider/agents/common_agent.py +159 -0
  12. bioguider/agents/common_agent_2step.py +126 -0
  13. bioguider/agents/common_step.py +85 -0
  14. bioguider/agents/dockergeneration_execute_step.py +186 -0
  15. bioguider/agents/dockergeneration_observe_step.py +153 -0
  16. bioguider/agents/dockergeneration_plan_step.py +158 -0
  17. bioguider/agents/dockergeneration_task.py +158 -0
  18. bioguider/agents/dockergeneration_task_utils.py +220 -0
  19. bioguider/agents/evaluation_task.py +269 -0
  20. bioguider/agents/identification_execute_step.py +179 -0
  21. bioguider/agents/identification_observe_step.py +92 -0
  22. bioguider/agents/identification_plan_step.py +135 -0
  23. bioguider/agents/identification_task.py +220 -0
  24. bioguider/agents/identification_task_utils.py +18 -0
  25. bioguider/agents/peo_common_step.py +64 -0
  26. bioguider/agents/prompt_utils.py +190 -0
  27. bioguider/agents/python_ast_repl_tool.py +69 -0
  28. bioguider/agents/rag_collection_task.py +130 -0
  29. bioguider/conversation.py +67 -0
  30. bioguider/database/summarized_file_db.py +140 -0
  31. bioguider/managers/evaluation_manager.py +108 -0
  32. bioguider/rag/__init__.py +0 -0
  33. bioguider/rag/config.py +117 -0
  34. bioguider/rag/data_pipeline.py +648 -0
  35. bioguider/rag/embedder.py +24 -0
  36. bioguider/rag/rag.py +134 -0
  37. bioguider/settings.py +103 -0
  38. bioguider/utils/constants.py +40 -0
  39. bioguider/utils/default.gitignore +140 -0
  40. bioguider/utils/file_utils.py +126 -0
  41. bioguider/utils/gitignore_checker.py +175 -0
  42. bioguider/utils/pyphen_utils.py +73 -0
  43. bioguider/utils/utils.py +27 -0
  44. bioguider-0.2.3.dist-info/LICENSE +21 -0
  45. bioguider-0.2.3.dist-info/METADATA +44 -0
  46. bioguider-0.2.3.dist-info/RECORD +47 -0
  47. bioguider-0.2.3.dist-info/WHEEL +4 -0
bioguider/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,88 @@
1
+
2
+ from typing import Callable
3
+ from abc import ABC, abstractmethod
4
+
5
+ from langchain_openai.chat_models.base import BaseChatOpenAI
6
+ from langgraph.graph.graph import CompiledGraph
7
+
8
+ from bioguider.utils.constants import DEFAULT_TOKEN_USAGE
9
+ from bioguider.database.summarized_file_db import SummarizedFilesDb
10
+
11
+ class AgentTask(ABC):
12
+ """
13
+ A class representing a step in an agent's process.
14
+ """
15
+
16
+ def __init__(self, llm: BaseChatOpenAI, step_callback: Callable | None = None):
17
+ """
18
+ Initialize the AgentStep with a language model and a callback function.
19
+
20
+ Args:
21
+ llm (BaseChatOpenAI): The language model to use.
22
+ step_callback (Callable): A callback function to handle step results.
23
+ """
24
+ self.llm = llm
25
+ self.step_callback = step_callback
26
+ self.summary_file_db = None
27
+ self.graph: CompiledGraph | None = None
28
+
29
+ def _print_step(
30
+ self,
31
+ step_name: str | None = None,
32
+ step_output: str | None = None,
33
+ token_usage: dict | object | None = None,
34
+ ):
35
+ if self.step_callback is None:
36
+ return
37
+ # convert token_usage to dict
38
+ if token_usage is not None and not isinstance(token_usage, dict):
39
+ token_usage = vars(token_usage)
40
+ token_usage = {**DEFAULT_TOKEN_USAGE, **token_usage}
41
+ step_callback = self.step_callback
42
+ step_callback(
43
+ step_name=step_name,
44
+ step_output=step_output,
45
+ token_usage=token_usage,
46
+ )
47
+
48
+ def compile(self, repo_path: str, gitignore_path: str, db: SummarizedFilesDb | None = None, **kwargs):
49
+ """
50
+ Compile the agent step with the given repository and gitignore paths.
51
+
52
+ Args:
53
+ repo_path (str): The path to the repository.
54
+ gitignore_path (str): The path to the .gitignore file.
55
+ **kwargs: derived class may pass more arguments to implmented _compile(), that is,
56
+ what **kwargs is depends on derived class
57
+ """
58
+ self.summary_file_db = db
59
+ self._compile(repo_path, gitignore_path, **kwargs)
60
+
61
+ @abstractmethod
62
+ def _compile(self, repo_path: str, gitignore_path: str, **kwargs):
63
+ """
64
+ Abstract method to compile the agent step.
65
+
66
+ Args:
67
+ repo_path (str): The path to the repository.
68
+ gitignore_path (str): The path to the .gitignore file.
69
+ """
70
+ pass
71
+
72
+ def _go_graph(self, input: dict) -> dict:
73
+ input = {
74
+ **input,
75
+ "llm": self.llm,
76
+ "step_output_callback": self.step_callback,
77
+ }
78
+ for s in self.graph.stream(
79
+ input=input,
80
+ stream_mode="values",
81
+ config={"recursion_limit": 500},
82
+ ):
83
+ print(s)
84
+
85
+ return s
86
+
87
+
88
+
@@ -0,0 +1,147 @@
1
+ import os
2
+ from typing import Callable
3
+ from langchain_openai.chat_models.base import BaseChatOpenAI
4
+ from bioguider.database.summarized_file_db import SummarizedFilesDb
5
+ from bioguider.utils.file_utils import get_file_type
6
+ from bioguider.agents.agent_utils import read_directory, read_file, summarize_file
7
+
8
+ class agent_tool:
9
+ def __init__(
10
+ self,
11
+ llm: BaseChatOpenAI | None = None,
12
+ output_callback:Callable[[dict], None] = None,
13
+ ):
14
+ self.llm = llm
15
+ self.output_callback = output_callback
16
+
17
+ def _print_token_usage(self, token_usage: dict):
18
+ if self.output_callback is not None:
19
+ self.output_callback(token_usage=token_usage)
20
+ def _print_step_output(self, step_output: str):
21
+ if self.output_callback is not None:
22
+ self.output_callback(step_output=step_output)
23
+
24
+ class read_file_tool:
25
+ """ read file
26
+ Args:
27
+ file_path str: file path
28
+ Returns:
29
+ A string of file content, if the file does not exist, return None.
30
+ """
31
+ def __init__(self, repo_path: str | None = None):
32
+ self.repo_path = repo_path if repo_path is not None else ""
33
+
34
+ def run(self, file_path: str) -> str | None:
35
+ if file_path is None:
36
+ return None
37
+ file_path = file_path.strip()
38
+ if self.repo_path is not None and self.repo_path not in file_path:
39
+ file_path = os.path.join(self.repo_path, file_path)
40
+ if not os.path.isfile(file_path):
41
+ return None
42
+ return read_file(file_path)
43
+
44
+ class summarize_file_tool(agent_tool):
45
+ """ read and summarize the file
46
+ Args:
47
+ file_path str: file path
48
+ Returns:
49
+ A string of summarized file content, if the file does not exist, return None.
50
+ """
51
+ def __init__(
52
+ self,
53
+ llm: BaseChatOpenAI,
54
+ repo_path: str | None = None,
55
+ output_callback: Callable | None = None,
56
+ detailed_level: int | None = 6,
57
+ db: SummarizedFilesDb | None = None,
58
+ summaize_instruction: str = "",
59
+ ):
60
+ super().__init__(llm=llm, output_callback=output_callback)
61
+ self.repo_path = repo_path
62
+ detailed_level = detailed_level if detailed_level is not None else 6
63
+ detailed_level = detailed_level if detailed_level > 0 else 1
64
+ detailed_level = detailed_level if detailed_level <= 10 else 10
65
+ self.detailed_level = detailed_level
66
+ self.summary_file_db = db
67
+ self.summarize_instruction = summaize_instruction
68
+
69
+ def _retrive_from_summary_file_db(self, file_path: str) -> str | None:
70
+ if self.summary_file_db is None:
71
+ return None
72
+ return self.summary_file_db.select_summarized_text(
73
+ file_path=file_path,
74
+ instruction=self.summarize_instruction,
75
+ summarize_level=self.detailed_level,
76
+ )
77
+ def _save_to_summary_file_db(self, file_path: str, summarized_text: str, token_usage: dict):
78
+ if self.summary_file_db is None:
79
+ return
80
+ self.summary_file_db.upsert_summarized_file(
81
+ file_path=file_path,
82
+ instruction=self.summarize_instruction,
83
+ summarize_level=self.detailed_level,
84
+ summarized_text=summarized_text,
85
+ token_usage=token_usage,
86
+ )
87
+ def run(self, file_path: str) -> str | None:
88
+ if file_path is None:
89
+ return None
90
+
91
+ file_path = file_path.strip()
92
+ abs_file_path = file_path
93
+ if self.repo_path is not None and self.repo_path not in abs_file_path:
94
+ abs_file_path = os.path.join(self.repo_path, abs_file_path)
95
+ if not os.path.isfile(abs_file_path):
96
+ return f"{file_path} is not a file."
97
+ summarized_content = self._retrive_from_summary_file_db(
98
+ file_path=file_path
99
+ )
100
+ if summarized_content is not None:
101
+ return f"summarized content of file {file_path}: " + summarized_content
102
+
103
+ file_content = read_file(abs_file_path)
104
+ file_content = file_content.replace("{", "{{").replace("}", "}}")
105
+ summarized_content, token_usage = summarize_file(
106
+ self.llm, abs_file_path, file_content, self.detailed_level,
107
+ summary_instructions=self.summarize_instruction,
108
+ )
109
+ self._save_to_summary_file_db(
110
+ file_path=file_path,
111
+ summarized_text=summarized_content,
112
+ token_usage=token_usage,
113
+ )
114
+ self._print_token_usage(token_usage)
115
+ return f"summarized content of file {file_path}: " + summarized_content
116
+
117
+ class read_directory_tool:
118
+ """Reads the contents of a directory, including files and subdirectories in it..
119
+ Args:
120
+ dir_path (str): Path to the directory.
121
+ Returns:
122
+ a string containing file and subdirectory paths found within the specified depth.
123
+ """
124
+ def __init__(
125
+ self,
126
+ repo_path: str | None = None,
127
+ gitignore_path: str | None = None,
128
+ ):
129
+ self.repo_path = repo_path
130
+ self.gitignore_path = gitignore_path if gitignore_path is not None else ""
131
+
132
+ def run(self, dir_path):
133
+ dir_path = dir_path.strip()
134
+ full_path = dir_path
135
+ if full_path == "." or full_path == "..":
136
+ return f"Please skip this folder {dir_path}"
137
+ if self.repo_path not in full_path:
138
+ full_path = os.path.join(self.repo_path, full_path)
139
+ files = read_directory(full_path, gitignore_path=self.gitignore_path, level=1)
140
+ if files is None:
141
+ return "N/A"
142
+ file_pairs = [(f, get_file_type(os.path.join(full_path, f)).value) for f in files]
143
+ dir_structure = ""
144
+ for f, f_type in file_pairs:
145
+ dir_structure += f"{os.path.join(dir_path, f)} - {f_type}\n"
146
+ return f"The 2-level content of directory {dir_path}: \n" + \
147
+ f"{dir_structure if len(dir_structure) > 0 else 'No files and sub-directories in it'}"
@@ -0,0 +1,357 @@
1
+
2
+ import json
3
+ import os
4
+ import re
5
+ import subprocess
6
+ from typing import List, Optional, Tuple, Union
7
+ from langchain_openai import AzureChatOpenAI
8
+ from langchain_deepseek import ChatDeepSeek
9
+ from langchain_core.utils.interactive_env import is_interactive_env
10
+ from langchain_core.messages.base import get_msg_title_repr
11
+ from langchain_core.prompts import ChatPromptTemplate, StringPromptTemplate
12
+ from langchain_core.messages import AIMessage
13
+ from langchain_openai.chat_models.base import BaseChatOpenAI
14
+ from langchain.tools import BaseTool
15
+ from langchain.schema import AgentAction, AgentFinish
16
+ from langchain.agents import AgentOutputParser
17
+ from langgraph.prebuilt import create_react_agent
18
+ import logging
19
+
20
+ from pydantic import BaseModel, Field
21
+
22
+ from bioguider.utils.constants import DEFAULT_TOKEN_USAGE
23
+ from bioguider.utils.file_utils import get_file_type
24
+ from ..utils.gitignore_checker import GitignoreChecker
25
+ from ..database.summarized_file_db import SummarizedFilesDb
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ class PlanAgentResult(BaseModel):
30
+ """ Identification Plan Result """
31
+ actions: list[dict] = Field(description="a list of action dictionary, e.g. [{'name': 'read_file', 'input': 'README.md'}, ...]")
32
+
33
+ PlanAgentResultJsonSchema = {
34
+ "title": "identification_plan_result",
35
+ "description": "plan result",
36
+ "type": "object",
37
+ "properties": {
38
+ "actions": {
39
+ "type": "array",
40
+ "description": """a list of action dictionary, e.g. [{'name': 'read_file', 'input': 'README.md'}, ...]""",
41
+ "title": "Actions",
42
+ "items": {"type": "object"}
43
+ },
44
+ },
45
+ "required": ["actions"],
46
+ }
47
+
48
+ def get_openai():
49
+ return get_llm(
50
+ api_key=os.environ.get("OPENAI_API_KEY"),
51
+ model_name=os.environ.get("OPENAI_MODEL"),
52
+ azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
53
+ api_version=os.environ.get("OPENAI_API_VERSION"),
54
+ azure_deployment=os.environ.get("OPENAI_DEPLOYMENT_NAME"),
55
+ max_tokens=os.environ.get("OPENAI_MAX_OUTPUT_TOKEN"),
56
+ )
57
+
58
+ def get_llm(
59
+ api_key: str,
60
+ model_name: str="gpt-4o",
61
+ azure_endpoint: str=None,
62
+ api_version: str=None,
63
+ azure_deployment: str=None,
64
+ temperature: float = 0.0,
65
+ max_tokens: int = 4096,
66
+ ):
67
+ if model_name.startswith("deepseek"):
68
+ chat = ChatDeepSeek(
69
+ api_key=api_key,
70
+ model=model_name,
71
+ temperature=temperature,
72
+ max_tokens=max_tokens,
73
+ )
74
+ elif model_name.startswith("gpt"):
75
+ chat = AzureChatOpenAI(
76
+ api_key=api_key,
77
+ azure_endpoint=azure_endpoint,
78
+ api_version=api_version,
79
+ azure_deployment=azure_deployment,
80
+ model=model_name,
81
+ temperature=temperature,
82
+ max_tokens=max_tokens,
83
+ )
84
+ else:
85
+ raise ValueError("Invalid model name")
86
+ # validate chat
87
+ try:
88
+ chat.invoke("Hi")
89
+ except Exception as e:
90
+ print(e)
91
+ return None
92
+ return chat
93
+
94
+ def pretty_print(message, printout = True):
95
+ if isinstance(message, tuple):
96
+ title = message
97
+ else:
98
+ if isinstance(message.content, list):
99
+ title = get_msg_title_repr(message.type.title().upper() + " Message", bold=is_interactive_env())
100
+ if message.name is not None:
101
+ title += f"\nName: {message.name}"
102
+
103
+ for i in message.content:
104
+ if i['type'] == 'text':
105
+ title += f"\n{i['text']}\n"
106
+ elif i['type'] == 'tool_use':
107
+ title += f"\nTool: {i['name']}"
108
+ title += f"\nInput: {i['input']}"
109
+ if printout:
110
+ print(f"{title}")
111
+ else:
112
+ title = get_msg_title_repr(message.type.title() + " Message", bold=is_interactive_env())
113
+ if message.name is not None:
114
+ title += f"\nName: {message.name}"
115
+ title += f"\n\n{message.content}"
116
+ if printout:
117
+ print(f"{title}")
118
+ return title
119
+
120
+ HUGE_FILE_LENGTH = 10 * 1024 # 10K
121
+
122
+ def read_file(
123
+ file_path: str,
124
+ ) -> str | None:
125
+ if not os.path.isfile(file_path):
126
+ return None
127
+ with open(file_path, 'r') as f:
128
+ content = f.read()
129
+ return content
130
+
131
+ def write_file(file_path: str, content: str):
132
+ try:
133
+ with open(file_path, "w") as fobj:
134
+ fobj.write(content)
135
+ return True
136
+ except Exception as e:
137
+ logger.error(e)
138
+ return False
139
+
140
+ def read_directory(
141
+ dir_path: str,
142
+ gitignore_path: str,
143
+ level: int=1,
144
+ ) -> list[str] | None:
145
+ if not os.path.isdir(dir_path):
146
+ return None
147
+ gitignore_checker = GitignoreChecker(
148
+ directory=dir_path,
149
+ gitignore_path=gitignore_path
150
+ )
151
+ files = gitignore_checker.check_files_and_folders(level=level)
152
+ return files
153
+
154
+
155
+ EVALUATION_SUMMARIZE_FILE_PROMPT = ChatPromptTemplate.from_template("""
156
+ You are provided with the content of the file **{file_name}**:
157
+ ```
158
+ {file_content}
159
+ ```
160
+ ### **Summary Instructions**
161
+ {summary_instructions}
162
+ The content is lengthy. Please generate a concise summary ({sentence_num1}-{sentence_num2} sentences).
163
+ """)
164
+
165
+ MAX_FILE_LENGTH=20 *1024 # 20K
166
+ MAX_SENTENCE_NUM=20
167
+ def summarize_file(
168
+ llm: BaseChatOpenAI,
169
+ name: str,
170
+ content: str | None = None,
171
+ level: int = 3,
172
+ summary_instructions: str | None = None,
173
+ db: SummarizedFilesDb | None = None,
174
+ ) -> Tuple[str, dict]:
175
+ if content is None:
176
+ try:
177
+ with open(name, "r") as fobj:
178
+ content = fobj.read()
179
+ except Exception as e:
180
+ logger.error(e)
181
+ return ""
182
+ # First, query from database
183
+ if db is not None:
184
+ res = db.select_summarized_text(name, summary_instructions, level)
185
+ if res is not None:
186
+ return res, {**DEFAULT_TOKEN_USAGE}
187
+
188
+ file_content = content
189
+ level = level if level > 0 else 1
190
+ level = level if level < MAX_SENTENCE_NUM+1 else MAX_SENTENCE_NUM
191
+ if len(file_content) > MAX_FILE_LENGTH:
192
+ file_content = content[:MAX_FILE_LENGTH] + " ..."
193
+ prompt = EVALUATION_SUMMARIZE_FILE_PROMPT.format(
194
+ file_name=name,
195
+ file_content=file_content,
196
+ sentence_num1=level,
197
+ sentence_num2=level+1,
198
+ summary_instructions=summary_instructions \
199
+ if summary_instructions is not None and len(summary_instructions) > 0 \
200
+ else "N/A",
201
+ )
202
+
203
+ config = {"recursion_limit": 500}
204
+ res: AIMessage = llm.invoke([("human", prompt)], config=config)
205
+ out = res.content
206
+ token_usage = {
207
+ "prompt_tokens": res.usage_metadata["input_tokens"],
208
+ "completion_tokens": res.usage_metadata["output_tokens"],
209
+ "total_tokens": res.usage_metadata["total_tokens"],
210
+ }
211
+ if db is not None:
212
+ db.upsert_summarized_file(
213
+ name, summary_instructions, level, token_usage
214
+ )
215
+
216
+ return out, token_usage
217
+
218
+ def increase_token_usage(
219
+ token_usage: Optional[dict] = None,
220
+ incremental: dict = {**DEFAULT_TOKEN_USAGE},
221
+ ):
222
+ if token_usage is None:
223
+ token_usage = {**DEFAULT_TOKEN_USAGE}
224
+ token_usage["total_tokens"] += incremental["total_tokens"]
225
+ token_usage["completion_tokens"] += incremental["completion_tokens"]
226
+ token_usage["prompt_tokens"] += incremental["prompt_tokens"]
227
+
228
+ return token_usage
229
+
230
+
231
+ # Set up a prompt template
232
+ class CustomPromptTemplate(StringPromptTemplate):
233
+ # The template to use
234
+ template: str
235
+ # The list of tools available
236
+ tools: List[BaseTool]
237
+ # Plan
238
+ plan_actions: str
239
+
240
+ def format(self, **kwargs) -> str:
241
+ # Get the intermediate steps (AgentAction, Observation tuples)
242
+ # Format them in a particular way
243
+ intermediate_steps = kwargs.pop("intermediate_steps")
244
+ thoughts = ""
245
+ for action, observation in intermediate_steps:
246
+ thoughts += action.log
247
+ thoughts += f"\nObservation: {observation}\n"
248
+ # Set plan_step
249
+ kwargs["plan_actions"] = self.plan_actions
250
+ # Set the agent_scratchpad variable to that value
251
+ kwargs["agent_scratchpad"] = thoughts
252
+ # Create a tools variable from the list of tools provided
253
+ kwargs["tools"] = "\n".join([f"{tool.name}: {tool.description}" for tool in self.tools])
254
+ # Create a list of tool names for the tools provided
255
+ kwargs["tool_names"] = ", ".join([tool.name for tool in self.tools])
256
+ prompt = self.template.format(**kwargs)
257
+ # print([prompt])
258
+ return prompt
259
+
260
+ class CustomOutputParser(AgentOutputParser):
261
+ def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
262
+ # Check if agent should finish
263
+ if "Final Answer:" in llm_output:
264
+ return AgentFinish(
265
+ return_values={"output": llm_output},
266
+ log=llm_output,
267
+ )
268
+ # Parse out the action and action input
269
+ regex = r"Action\s*\d*\s*:(.*?)\nAction\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
270
+ match = re.search(regex, llm_output, re.DOTALL)
271
+ if not match:
272
+ # raise ValueError(f"Could not parse LLM output: `{llm_output}`")
273
+ print(f"Warning: could not parse LLM output: `{llm_output}`, finishing chain...")
274
+ return AgentFinish(
275
+ return_values={"output": llm_output},
276
+ log=llm_output,
277
+ )
278
+ action = match.group(1).strip()
279
+ action_input = match.group(2)
280
+ # Return the action and action input
281
+ action_dict = None
282
+ action_input = action_input.strip(" ").strip('"')
283
+ action_input_replaced = action_input.replace("'", '"')
284
+ try:
285
+ action_dict = json.loads(action_input_replaced)
286
+ except json.JSONDecodeError:
287
+ pass
288
+ if action_dict is None:
289
+ # try using ast to parse input string
290
+ import ast
291
+ try:
292
+ action_dict = ast.literal_eval(action_input)
293
+ if not isinstance(action_dict, dict):
294
+ action_dict = None
295
+ except Exception as e:
296
+ pass
297
+ return AgentAction(
298
+ tool=action,
299
+ tool_input=action_dict if action_dict is not None else action_input,
300
+ log=llm_output
301
+ )
302
+
303
+ def get_tool_names_and_descriptions(tools: List[BaseTool]) -> str:
304
+ tool_names = []
305
+ tools_descriptions = ""
306
+ for tool in tools:
307
+ tools_descriptions += f"name: {tool.name}, description: {tool.description}\n"
308
+ tool_names.append(tool.name)
309
+ return str(tool_names), tools_descriptions
310
+
311
+ def generate_repo_structure_prompt(
312
+ files: List[str],
313
+ dir_path: str="",
314
+ ) -> str:
315
+ # Convert the repo structure to a string
316
+ file_pairs = [(f, get_file_type(os.path.join(dir_path, f)).value) for f in files]
317
+ repo_structure = ""
318
+ for f, f_type in file_pairs:
319
+ repo_structure += f"{f} - {f_type}\n"
320
+ return repo_structure
321
+
322
+ class ObservationResult(BaseModel):
323
+ Analysis: Optional[str]=Field(description="Analyzing the goal, repository file structure and intermediate output.")
324
+ FinalAnswer: Optional[str]=Field(description="the final answer for the goal")
325
+ Thoughts: Optional[str]=Field(description="If the information is insufficient, the thoughts will be given and be taken into consideration in next round.")
326
+
327
+ def convert_plan_to_string(plan: PlanAgentResult) -> str:
328
+ plan_str = ""
329
+ for action in plan.actions:
330
+ action_str = f"Step: {action['name']}\n"
331
+ action_str += f"Step Input: {action['input']}\n"
332
+ plan_str += action_str
333
+ return plan_str
334
+
335
+ def run_command(command: list, cwd: str = None, timeout: int = None):
336
+ """
337
+ Run a shell command with optional timeout and return stdout, stderr, and return code.
338
+ """
339
+ try:
340
+ result = subprocess.run(
341
+ command,
342
+ cwd=cwd,
343
+ stdout=subprocess.PIPE,
344
+ stderr=subprocess.PIPE,
345
+ text=True,
346
+ timeout=timeout
347
+ )
348
+ return result.stdout, result.stderr, result.returncode
349
+ except subprocess.TimeoutExpired as e:
350
+ return e.stdout or "", e.stderr or f"Command timed out after {timeout} seconds", -1
351
+
352
+ def escape_braces(text: str) -> str:
353
+ # First replace single } not part of }} with }}
354
+ text = re.sub(r'(?<!})}(?!})', '}}', text)
355
+ # Then replace single { not part of {{
356
+ text = re.sub(r'(?<!{){(?!{)', '{{', text)
357
+ return text