Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 56 additions & 5 deletions src/microbots/MicroBot.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@
{
task_done: true | false,
command: "<command to run> | null",
result: str | null
result: str | null,
reasoning: "Brief explanation of what you're thinking and why you're running this command"
}
```
"""

system_prompt_common = """There is a shell session open for you.
I will provide a task to achieve using the shell.
You will provide the commands to achieve the task in this particular below json format, Ensure all the time to respond in this format only and nothing else, also all the properties ( task_done, command, result ) are mandatory on each response
You will provide the commands to achieve the task in this particular below json format, Ensure all the time to respond in this format only and nothing else, also all the properties ( task_done, command, result, reasoning ) are mandatory on each response
{llm_output_format}
after each command I will provide the output of the command.

IMPORTANT: Always include 'reasoning' field to explain your thought process before executing each command.
ensure to run only one command at a time.
I won't be able to intervene once I have given task. ."""

Expand Down Expand Up @@ -105,9 +108,28 @@ def run(self, task, max_iterations=20, timeout_in_seconds=200) -> BotRunResult:
result=None,
error="Did not complete",
)
logger.info("%s TASK STARTED : %s...", LogLevelEmoji.INFO, task[0:15])
print(f"\n{'='*80}")
print(f"🚀 TASK STARTED: {task}")
print(f"{'='*80}")
Comment on lines +111 to +113
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's convert all print statements into INFO log statements. To avoid being unresponsive in the console while running, add a WARNING log to mention Log Level is above INFO. So, no output will be printed here during normal operation. Please wait patiently.

It gives us better control over the output and log stream. Particularly useful to run in quiet mode.


logger.info("%s TASK STARTED : %s...", LogLevelEmoji.INFO, task[0:50])

while llm_response.task_done is False:
print(f"\n{'─'*80}")
print(f"🔄 STEP {iteration_count}")
print(f"{'─'*80}")

# Show LLM's thought process
print(f"\n💭 LLM ANALYSIS:")
if hasattr(llm_response, 'reasoning') and llm_response.reasoning:
print(f" {llm_response.reasoning}")
else:
print(f" Analyzing current situation and determining next action...")

# Show the command to execute
print(f"\n⚡ COMMAND TO EXECUTE:")
print(f" {LogTextColor.OKBLUE}{llm_response.command}{LogTextColor.ENDC}")

logger.info("%s Step-%d %s", "-" * 20, iteration_count, "-" * 20)
logger.info(
f" ➡️ LLM tool call : {LogTextColor.OKBLUE}{json.dumps(llm_response.command)}{LogTextColor.ENDC}",
Expand All @@ -132,11 +154,27 @@ def run(self, task, max_iterations=20, timeout_in_seconds=200) -> BotRunResult:
return return_value

llm_command_output = self.environment.execute(llm_response.command)

# Show command output clearly
print(f"\n📤 COMMAND OUTPUT:")
if llm_command_output.stdout:
print(f" ✅ STDOUT:")
for line in llm_command_output.stdout.split('\n'):
if line.strip():
print(f" {line}")
logger.info(
" ⬅️ Command Execution Output: %s",
llm_command_output.stdout,
)

if llm_command_output.stderr:
print(f" ❌ STDERR:")
for line in llm_command_output.stderr.split('\n'):
if line.strip():
print(f" {line}")

if not llm_command_output.stdout and not llm_command_output.stderr:
print(f" ℹ️ No output received")

# Convert CmdReturn to string for LLM
if llm_command_output.stdout:
Expand All @@ -146,9 +184,22 @@ def run(self, task, max_iterations=20, timeout_in_seconds=200) -> BotRunResult:
else:
output_text = "No output received"

print(f"\n🔄 SENDING OUTPUT TO LLM...")
llm_response = self.llm.ask(output_text)

logger.info("🔚 TASK COMPLETED : %s...", task[0:15])
print(f"\n{'='*80}")
print(f"✅ TASK COMPLETED!")
print(f"{'='*80}")
print(f"\n🎯 FINAL RESULT:")
if llm_response.result:
for line in str(llm_response.result).split('\n'):
if line.strip():
print(f" {line}")
else:
print(" Task completed successfully")
print(f"\n{'='*80}")

logger.info("🔚 TASK COMPLETED : %s...", task[0:50])
return BotRunResult(status=True, result=llm_response.result, error=None)

# TODO : pass the sandbox path
Expand All @@ -167,7 +218,7 @@ def _create_environment(self, folder_to_mount: Optional[Mount]):
)

def _create_llm(self):
if self.model_provider == ModelProvider.OPENAI:
if self.model_provider in [ModelProvider.OPENAI, ModelProvider.OPENAI_STANDARD]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ModelProvider is an internal class. To use ModelProvider.OPENAI_STANDARD, You need to introduce that enum in the constants.py file.

self.llm = OpenAIApi(
system_prompt=self.system_prompt, deployment_name=self.deployment_name
)
Expand Down
13 changes: 8 additions & 5 deletions src/microbots/llm/openai_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class llmAskResponse:
task_done: bool = False
command: str = ""
result: str | None = None
reasoning: str | None = None


class OpenAIApi:
Expand All @@ -38,12 +39,12 @@ def ask(self, message) -> llmAskResponse:
self.messages.append({"role": "user", "content": message})
return_value = {}
while self._validate_llm_response(return_value) is False:
response = self.ai_client.responses.create(
response = self.ai_client.chat.completions.create(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our intention to use responses api over chat_completion api is to depending on the model itself to maintain the context using store=true. We're yet to invest and implement that feature in detail. So, I would recommend you to stick with responses api itself as much as possible.

If you still wish to stick with chat_completion, please create a new class OpenAIChatCompletionAPI class and implement your changes. When introducing a new such class, please create one Abstract class and implement it in both of the API classes (Later we'll move to Factory design patter or Pydantic based models).

We'll have provision to include appropriate API class in CustomBots. So, the user can choose their required API class based on the model they use.

model=self.deployment_name,
input=self.messages,
messages=self.messages,
)
try:
return_value = json.loads(response.output_text)
return_value = json.loads(response.choices[0].message.content)
except Exception as e:
logger.error(
f"%s Error occurred while dumping JSON: {e}", LogLevelEmoji.ERROR
Expand All @@ -52,14 +53,15 @@ def ask(self, message) -> llmAskResponse:
"%s Failed to parse JSON from LLM response and the response is",
LogLevelEmoji.ERROR,
)
logger.error(response.output_text)
logger.error(response.choices[0].message.content)

self.messages.append({"role": "assistant", "content": json.dumps(return_value)})

return llmAskResponse(
task_done=return_value["task_done"],
result=return_value["result"],
command=return_value["command"],
reasoning=return_value.get("reasoning", "No reasoning provided"),
)

def clear_history(self):
Expand All @@ -72,7 +74,8 @@ def clear_history(self):
return True

def _validate_llm_response(self, response: dict) -> bool:
if "task_done" in response and "command" in response and "result" in response:
required_fields = ["task_done", "command", "result"]
if all(field in response for field in required_fields):
logger.info("The llm response is %s ", response)
return True
return False