Feat : adding work that was done locally

This commit is contained in:
martin legrand
2025-02-19 19:07:24 +01:00
parent b64c75afeb
commit 62cc76764e
17 changed files with 1488 additions and 21 deletions
+119
View File
@@ -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, Im 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
+40
View File
@@ -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)
+124
View File
@@ -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
+36
View File
@@ -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 ?")
+89
View File
@@ -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"]))
+80
View File
@@ -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))
+4
View File
@@ -0,0 +1,4 @@
from .PyInterpreter import PyInterpreter
from .BashInterpreter import BashInterpreter
__all__ = ["PyInterpreter", "BashInterpreter"]
+94
View File
@@ -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
+20
View File
@@ -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)