Feat : adding work that was done locally
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
from typing import Tuple, Callable
|
||||
from abc import abstractmethod
|
||||
import os
|
||||
import random
|
||||
|
||||
class Agent():
|
||||
def __init__(self, model: str,
|
||||
name: str,
|
||||
prompt_path:str,
|
||||
provider) -> None:
|
||||
self._name = name
|
||||
self._current_directory = os.getcwd()
|
||||
self._model = model
|
||||
self._llm = provider
|
||||
self._history = []
|
||||
self._tools = {}
|
||||
self.set_system_prompt(prompt_path)
|
||||
|
||||
def set_system_prompt(self, prompt_path: str) -> None:
|
||||
self.set_history(self.load_prompt(prompt_path))
|
||||
|
||||
@property
|
||||
def history(self):
|
||||
return self._history
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def get_tools(self) -> dict:
|
||||
return self._tools
|
||||
|
||||
def set_history(self, system_prompt: str) -> None:
|
||||
"""
|
||||
Set the default history for the agent.
|
||||
Deepseek developers recommand not using a system prompt directly.
|
||||
We therefore pass the system prompt as a user message.
|
||||
"""
|
||||
self._history = [{'role': 'user', 'content': system_prompt},
|
||||
{'role': 'assistant', 'content': f'Hello, How can I help you today ?'}]
|
||||
|
||||
def add_to_history(self, role: str, content: str) -> None:
|
||||
self._history.append({'role': role, 'content': content})
|
||||
|
||||
def clear_history(self) -> None:
|
||||
self._history = []
|
||||
|
||||
def add_tool(self, name: str, tool: Callable) -> None:
|
||||
if tool is not Callable:
|
||||
raise TypeError("Tool must be a callable object (a method)")
|
||||
self._tools[name] = tool
|
||||
|
||||
def load_prompt(self, file_path: str) -> str:
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Prompt file not found at path: {file_path}")
|
||||
except PermissionError:
|
||||
raise PermissionError(f"Permission denied to read prompt file at path: {file_path}")
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@abstractmethod
|
||||
def answer(self, prompt, speech_module) -> str:
|
||||
"""
|
||||
abstract method, implementation in child class.
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_reasoning_text(self, text: str) -> None:
|
||||
end_tag = "</think>"
|
||||
end_idx = text.rfind(end_tag)+8
|
||||
return text[end_idx:]
|
||||
|
||||
def extract_reasoning_text(self, text: str) -> None:
|
||||
start_tag = "<think>"
|
||||
end_tag = "</think>"
|
||||
start_idx = text.find(start_tag)
|
||||
end_idx = text.rfind(end_tag)+8
|
||||
return text[start_idx:end_idx]
|
||||
|
||||
def llm_request(self, history, verbose = True) -> Tuple[str, str]:
|
||||
thought = self._llm.respond(history, verbose)
|
||||
|
||||
reasoning = self.extract_reasoning_text(thought)
|
||||
answer = self.remove_reasoning_text(thought)
|
||||
self.add_to_history('assistant', answer)
|
||||
return answer, reasoning
|
||||
|
||||
def wait_message(self, speech_module):
|
||||
messages = ["Please be patient sir, I am working on it.",
|
||||
"At it, sir. In the meantime, how about a joke?",
|
||||
"Computing... I recommand you have a coffee while I work.",
|
||||
"Hold on, I’m crunching numbers.",
|
||||
"Working on it sir, please let me think."]
|
||||
speech_module.speak(messages[random.randint(0, len(messages)-1)])
|
||||
|
||||
def execute_modules(self, answer: str) -> Tuple[bool, str]:
|
||||
feedback = ""
|
||||
blocks = None
|
||||
|
||||
print("Loading tools: ", self._tools.items())
|
||||
for name, tool in self._tools.items():
|
||||
feedback = ""
|
||||
blocks = tool.load_exec_block(answer)
|
||||
|
||||
if blocks != None:
|
||||
output = tool.execute(blocks)
|
||||
feedback = tool.interpreter_feedback(output)
|
||||
answer = tool.remove_block(answer)
|
||||
self.add_to_history('user', feedback)
|
||||
|
||||
if "failure" in feedback.lower():
|
||||
return False, feedback
|
||||
if blocks == None:
|
||||
return True, feedback
|
||||
return True, feedback
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
from sources.tools import PyInterpreter, BashInterpreter
|
||||
from sources.utility import pretty_print
|
||||
from sources.agent import Agent
|
||||
|
||||
class CoderAgent(Agent):
|
||||
def __init__(self, model, name, prompt_path, provider):
|
||||
super().__init__(model, name, prompt_path, provider)
|
||||
self.set_system_prompt(prompt_path)
|
||||
self._tools = {
|
||||
"bash": BashInterpreter(),
|
||||
"python": PyInterpreter()
|
||||
}
|
||||
|
||||
def answer(self, prompt, speech_module) -> str:
|
||||
answer = ""
|
||||
attempt = 0
|
||||
max_attempts = 3
|
||||
self.add_to_history('user', prompt)
|
||||
|
||||
while attempt < max_attempts:
|
||||
pretty_print("Thinking...", color="status")
|
||||
self.wait_message(speech_module)
|
||||
answer, reasoning = self.llm_request(self.history)
|
||||
exec_success, feedback = self.execute_modules(answer)
|
||||
pretty_print(feedback, color="failure" if "failure" in feedback.lower() else "success")
|
||||
if exec_success:
|
||||
break
|
||||
attempt += 1
|
||||
|
||||
return answer, reasoning
|
||||
|
||||
if __name__ == "__main__":
|
||||
from llm_provider import Provider
|
||||
|
||||
#local_provider = Provider("ollama", "deepseek-r1:14b", None)
|
||||
server_provider = Provider("server", "deepseek-r1:14b", "192.168.1.100:5000")
|
||||
agent = CoderAgent("deepseek-r1:14b", "jarvis", "prompts/coder_agent.txt", server_provider)
|
||||
ans = agent.answer("What is the output of 5+5 in python ?")
|
||||
print(ans)
|
||||
@@ -0,0 +1,124 @@
|
||||
|
||||
import time
|
||||
import ollama
|
||||
from ollama import chat
|
||||
import requests
|
||||
import subprocess
|
||||
import ipaddress
|
||||
import platform
|
||||
|
||||
class Provider:
|
||||
def __init__(self, provider_name, model, server_address = "127.0.0.1:5000"):
|
||||
self.provider_name = provider_name.lower()
|
||||
self.model = model
|
||||
self.server = self.check_address_format(server_address)
|
||||
self.available_providers = {
|
||||
"ollama": self.ollama_fn,
|
||||
"server": self.server_fn,
|
||||
"test": self.test_fn
|
||||
|
||||
}
|
||||
if self.server != "":
|
||||
print("Provider initialized at ", self.server)
|
||||
else:
|
||||
print("Using localhost as provider")
|
||||
|
||||
def check_address_format(self, address):
|
||||
"""
|
||||
Validate if the address is valid IP.
|
||||
"""
|
||||
try:
|
||||
ip, port = address.rsplit(":", 1)
|
||||
ipaddress.ip_address(ip)
|
||||
if not port.isdigit() or not (0 <= int(port) <= 65535):
|
||||
raise ValueError("Port must be a number between 0 and 65535.")
|
||||
except ValueError as e:
|
||||
raise Exception(f"Invalid address format: {e}")
|
||||
return address
|
||||
|
||||
def respond(self, history, verbose = True):
|
||||
"""
|
||||
Use the choosen provider to generate text.
|
||||
"""
|
||||
llm = self.available_providers[self.provider_name]
|
||||
thought = llm(history, verbose)
|
||||
return thought
|
||||
|
||||
def is_ip_online(self, ip_address):
|
||||
"""
|
||||
Check if an IP address is online by sending a ping request.
|
||||
"""
|
||||
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
||||
command = ['ping', param, '1', ip_address]
|
||||
try:
|
||||
output = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5)
|
||||
if output.returncode == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
return False
|
||||
|
||||
def server_fn(self, history, verbose = True):
|
||||
"""
|
||||
Use a remote server wit LLM to generate text.
|
||||
"""
|
||||
thought = ""
|
||||
route_start = f"http://{self.server}/generate"
|
||||
|
||||
if not self.is_ip_online(self.server.split(":")[0]):
|
||||
raise Exception(f"Server is offline at {self.server}")
|
||||
|
||||
requests.post(route_start, json={"messages": history})
|
||||
is_complete = False
|
||||
while not is_complete:
|
||||
response = requests.get(f"http://{self.server}/get_updated_sentence")
|
||||
thought = response.json()["sentence"]
|
||||
# TODO add real time streaming to stdout
|
||||
is_complete = bool(response.json()["is_complete"])
|
||||
time.sleep(2)
|
||||
return thought
|
||||
|
||||
def ollama_fn(self, history, verbose = True):
|
||||
"""
|
||||
Use local ollama server to generate text.
|
||||
"""
|
||||
thought = ""
|
||||
try:
|
||||
stream = chat(
|
||||
model=self.model,
|
||||
messages=history,
|
||||
stream=True,
|
||||
)
|
||||
for chunk in stream:
|
||||
if verbose:
|
||||
print(chunk['message']['content'], end='', flush=True)
|
||||
thought += chunk['message']['content']
|
||||
except ollama.ResponseError as e:
|
||||
if e.status_code == 404:
|
||||
ollama.pull(self._model)
|
||||
if "refused" in str(e):
|
||||
raise Exception("Ollama connection failed. is the server running ?")
|
||||
raise e
|
||||
return thought
|
||||
|
||||
def test_fn(self, history, verbose = True):
|
||||
"""
|
||||
Test function to generate text.
|
||||
"""
|
||||
thought = """
|
||||
This is a test response from the test provider.
|
||||
Change provider to 'ollama' or 'server' to get real responses.
|
||||
|
||||
```python
|
||||
print("Hello world from python")
|
||||
```
|
||||
|
||||
```bash
|
||||
echo "Hello world from bash"
|
||||
```
|
||||
"""
|
||||
return thought
|
||||
@@ -0,0 +1,36 @@
|
||||
from kokoro import KPipeline
|
||||
from IPython.display import display, Audio
|
||||
import soundfile as sf
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
# 🇺🇸 'a' => American English, 🇬🇧 'b' => British English
|
||||
# 🇯🇵 'j' => Japanese: pip install misaki[ja]
|
||||
# 🇨🇳 'z' => Mandarin Chinese: pip install misaki[zh]
|
||||
pipeline = KPipeline(lang_code='a') # <= make sure lang_code matches voice
|
||||
|
||||
class Speech():
|
||||
def __init__(self) -> None:
|
||||
self.pipeline = KPipeline(lang_code='a')
|
||||
|
||||
def speak(self, sentence):
|
||||
sentence = self.clean_sentence(sentence)
|
||||
generator = self.pipeline(
|
||||
sentence, voice='af_heart', # <= change voice here
|
||||
speed=1, split_pattern=r'\n+'
|
||||
)
|
||||
for i, (gs, ps, audio) in enumerate(generator):
|
||||
audio_file = f'sample.wav'
|
||||
display(Audio(data=audio, rate=24000, autoplay=i==0))
|
||||
sf.write(audio_file, audio, 24000) # save each audio file
|
||||
subprocess.call(["afplay", audio_file])
|
||||
|
||||
def clean_sentence(self, sentence):
|
||||
sentence = re.sub(r'`.*?`', '', sentence)
|
||||
sentence = re.sub(r'[^a-zA-Z0-9.,!? ]+', '', sentence)
|
||||
sentence = re.sub(r'\s+', ' ', sentence).strip()
|
||||
return sentence
|
||||
|
||||
if __name__ == "__main__":
|
||||
speech = Speech()
|
||||
speech.speak("hello would you like coffee ?")
|
||||
@@ -0,0 +1,89 @@
|
||||
|
||||
import sys
|
||||
import re
|
||||
from io import StringIO
|
||||
import subprocess
|
||||
|
||||
if __name__ == "__main__":
|
||||
from tools import Tools
|
||||
else:
|
||||
from sources.tools.tools import Tools
|
||||
|
||||
class BashInterpreter(Tools):
|
||||
"""
|
||||
This class is a tool to allow agent for bash code execution.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.tag = "bash"
|
||||
|
||||
def execute(self, commands: str, safety = False, timeout = 10):
|
||||
"""
|
||||
Execute bash commands.
|
||||
"""
|
||||
if safety and input("Execute command? y/n ") != "y":
|
||||
return "Command rejected by user."
|
||||
|
||||
for command in commands:
|
||||
try:
|
||||
output = subprocess.check_output(command,
|
||||
shell=True,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
timeout=timeout
|
||||
)
|
||||
return output.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
return f"Command execution failed:\n{e.output}"
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"Command timed out. Output:\n{e.output}"
|
||||
|
||||
def interpreter_feedback(self, output):
|
||||
"""
|
||||
Provide feedback based on the output of the bash interpreter
|
||||
"""
|
||||
if self.execution_failure_check(output):
|
||||
feedback = f"[failure] Error in execution:\n{output}"
|
||||
else:
|
||||
feedback = "[success] Execution success, code output:\n" + output
|
||||
return feedback
|
||||
|
||||
def execution_failure_check(self, feedback):
|
||||
"""
|
||||
check if bash command failed.
|
||||
"""
|
||||
error_patterns = [
|
||||
r"expected",
|
||||
r"errno",
|
||||
r"failed",
|
||||
r"invalid",
|
||||
r"unrecognized",
|
||||
r"exception",
|
||||
r"syntax",
|
||||
r"segmentation fault",
|
||||
r"core dumped",
|
||||
r"unexpected",
|
||||
r"denied",
|
||||
r"not recognized",
|
||||
r"not permitted",
|
||||
r"not installed",
|
||||
r"not found",
|
||||
r"no such",
|
||||
r"too many",
|
||||
r"too few",
|
||||
r"busy",
|
||||
r"broken pipe",
|
||||
r"missing",
|
||||
r"undefined",
|
||||
r"refused",
|
||||
r"unreachable",
|
||||
r"not known"
|
||||
]
|
||||
combined_pattern = "|".join(error_patterns)
|
||||
if re.search(combined_pattern, feedback, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
bash = BashInterpreter()
|
||||
print(bash.execute(["ls", "pwd"]))
|
||||
@@ -0,0 +1,80 @@
|
||||
|
||||
import sys
|
||||
import re
|
||||
from io import StringIO
|
||||
|
||||
if __name__ == "__main__":
|
||||
from tools import Tools
|
||||
else:
|
||||
from sources.tools.tools import Tools
|
||||
|
||||
class PyInterpreter(Tools):
|
||||
"""
|
||||
This class is a tool to allow agent for python code execution.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.tag = "python"
|
||||
|
||||
def execute(self, codes:str, safety = False) -> str:
|
||||
"""
|
||||
Execute python code.
|
||||
"""
|
||||
output = ""
|
||||
if safety and input("Execute code ? y/n") != "y":
|
||||
return "Code rejected by user."
|
||||
stdout_buffer = StringIO()
|
||||
sys.stdout = stdout_buffer
|
||||
code = '\n\n'.join(codes)
|
||||
try:
|
||||
try:
|
||||
buffer = exec(code)
|
||||
if buffer is not None:
|
||||
output = buffer + '\n'
|
||||
except Exception as e:
|
||||
return "code execution failed:" + str(e)
|
||||
output = stdout_buffer.getvalue()
|
||||
finally:
|
||||
sys.stdout = sys.__stdout__
|
||||
return output
|
||||
|
||||
def interpreter_feedback(self, output:str) -> str:
|
||||
"""
|
||||
Provide feedback based on the output of the code execution
|
||||
"""
|
||||
if self.execution_failure_check(output):
|
||||
feedback = f"[failure] Error in execution:\n{output}"
|
||||
else:
|
||||
feedback = "[success] Execution success, code output:\n" + output
|
||||
return feedback
|
||||
|
||||
def execution_failure_check(self, feedback:str) -> bool:
|
||||
"""
|
||||
Check if the code execution failed.
|
||||
"""
|
||||
error_patterns = [
|
||||
r"expected",
|
||||
r"errno",
|
||||
r"failed",
|
||||
r"traceback",
|
||||
r"invalid",
|
||||
r"unrecognized",
|
||||
r"exception",
|
||||
r"syntax",
|
||||
r"crash",
|
||||
r"segmentation fault",
|
||||
r"core dumped"
|
||||
]
|
||||
combined_pattern = "|".join(error_patterns)
|
||||
if re.search(combined_pattern, feedback, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
codes = ["""
|
||||
def test():
|
||||
print("Hello world")
|
||||
test()
|
||||
"""]
|
||||
py = PyInterpreter()
|
||||
print(py.execute(codes))
|
||||
@@ -0,0 +1,4 @@
|
||||
from .PyInterpreter import PyInterpreter
|
||||
from .BashInterpreter import BashInterpreter
|
||||
|
||||
__all__ = ["PyInterpreter", "BashInterpreter"]
|
||||
@@ -0,0 +1,94 @@
|
||||
|
||||
"""
|
||||
define a generic tool class, any tool can be used by the agent.
|
||||
|
||||
A tool can be used by deepseek like so:
|
||||
```<tool name>
|
||||
<code or query to execute>
|
||||
```
|
||||
|
||||
For example:
|
||||
```python
|
||||
print("Hello world")
|
||||
```
|
||||
|
||||
This is then executed by the tool with its own class implementation of execute().
|
||||
|
||||
A tool is not just for code tool but also API, internet, etc..
|
||||
For example a flight API tool could be used like so:
|
||||
```flight_search
|
||||
HU787
|
||||
```
|
||||
"""
|
||||
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
|
||||
sys.path.append('..')
|
||||
|
||||
class Tools():
|
||||
"""
|
||||
Abstract class for all tools.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.tag = "undefined"
|
||||
self.api_key = None
|
||||
self.client = None
|
||||
self.messages = []
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, codes:str, safety:bool) -> str:
|
||||
"""
|
||||
abstract method, implementation in child class.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def execution_failure_check(self, output:str) -> bool:
|
||||
"""
|
||||
abstract method, implementation in child class.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def interpreter_feedback(self, output:str) -> str:
|
||||
"""
|
||||
abstract method, implementation in child class.
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_block(self, text:str) -> str:
|
||||
"""
|
||||
Remove all code/query blocks within a tag from the answer text.
|
||||
"""
|
||||
assert self.tag != "undefined", "Tag not defined"
|
||||
start_tag = f'```{self.tag}'
|
||||
end_tag = '```'
|
||||
start_idx = text.find(start_tag)
|
||||
end_idx = text.rfind(end_tag)+3
|
||||
if start_idx == -1 or end_idx == -1:
|
||||
return text
|
||||
return text[:start_idx] + text[end_idx:]
|
||||
|
||||
def load_exec_block(self, generation:str) -> str:
|
||||
"""
|
||||
Extract the code/query blocks from the answer text.
|
||||
"""
|
||||
assert self.tag != "undefined", "Tag not defined"
|
||||
start_tag = f'```{self.tag}'
|
||||
end_tag = '```'
|
||||
code_blocks = []
|
||||
start_index = 0
|
||||
|
||||
if start_tag not in generation:
|
||||
return None
|
||||
while True:
|
||||
start_pos = generation.find(start_tag, start_index)
|
||||
if start_pos == -1:
|
||||
break
|
||||
end_pos = generation.find(end_tag, start_pos + len(start_tag))
|
||||
if end_pos == -1:
|
||||
break
|
||||
code_blocks.append(generation[start_pos + len(start_tag):end_pos])
|
||||
start_index = end_pos + len(end_tag)
|
||||
return code_blocks
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
from colorama import Fore
|
||||
|
||||
def pretty_print(text, color = "info"):
|
||||
"""
|
||||
print text with color
|
||||
"""
|
||||
color_map = {
|
||||
"success": Fore.GREEN,
|
||||
"failure": Fore.RED,
|
||||
"status": Fore.LIGHTGREEN_EX,
|
||||
"code": Fore.LIGHTBLUE_EX,
|
||||
"warning": Fore.YELLOW,
|
||||
"output": Fore.LIGHTCYAN_EX,
|
||||
}
|
||||
if color not in color_map:
|
||||
print(text)
|
||||
pretty_print("Invalid color in pretty_print", "warning")
|
||||
return
|
||||
print(color_map[color], text, Fore.RESET)
|
||||
Reference in New Issue
Block a user