Upload 13 files
Browse files- .gitattributes +4 -0
- README.md +55 -3
- agi/Qwen3-4B-Instruct-2507-Q3_K_S.gguf +3 -0
- agi/agent_config.py +80 -0
- agi/assets/AGI.png +3 -0
- agi/assets/ArtificialGeneralIntelligence.pdf +3 -0
- agi/assets/sophos.png +3 -0
- agi/sophos.py +255 -0
- agi/sophos_models.py +33 -0
- agi/sophos_tools.py +278 -0
- agi/tool_categories.py +69 -0
- agi_gui.py +206 -0
- main.py +20 -0
- simple_test.py +15 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
agi/assets/AGI.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
agi/assets/ArtificialGeneralIntelligence.pdf filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
agi/assets/sophos.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
agi/Qwen3-4B-Instruct-2507-Q3_K_S.gguf filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
| 1 |
-
---
|
| 2 |
-
license: mit
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
base_model:
|
| 6 |
+
- Qwen/Qwen3-4B-Instruct-2507
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# AGI-5
|
| 10 |
+
## Artificial General Intelligence
|
| 11 |
+
|
| 12 |
+
<img src="agi/assets/AGI.png">
|
| 13 |
+
|
| 14 |
+
## Overview
|
| 15 |
+
This repository contains an implementation of a modular-agentic architecture for Artificial General Intelligence (AGI). The architecture is designed to facilitate the development of autonomous agents capable of complex reasoning, learning, and interaction with their environment.
|
| 16 |
+
|
| 17 |
+
## GUI
|
| 18 |
+
|
| 19 |
+
<img src="agi/assets/GUI.png">
|
| 20 |
+
|
| 21 |
+
This GUI allows users to interact with the AGI agent through a user-friendly interface. Users can input prompts, view agent responses, and monitor the agent's internal state and tool usage.
|
| 22 |
+
|
| 23 |
+
# Example
|
| 24 |
+
|
| 25 |
+
```python
|
| 26 |
+
from agi.sophos import Agent
|
| 27 |
+
from agi.sophos_tools import *
|
| 28 |
+
|
| 29 |
+
toolbox = all_tools()
|
| 30 |
+
|
| 31 |
+
agent = Agent(
|
| 32 |
+
name="Sophos Agent",
|
| 33 |
+
instructions="You are an AI Agent.",
|
| 34 |
+
model="agi/Qwen3-4B-Instruct-2507-Q3_K_S.gguf",
|
| 35 |
+
tools=toolbox,
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
prompt = "Roll a dice, also whats the weather in Tokyo?"
|
| 39 |
+
response = agent.run(prompt)
|
| 40 |
+
print(response)
|
| 41 |
+
```
|
| 42 |
+
### Output:
|
| 43 |
+
```python
|
| 44 |
+
|
| 45 |
+
"""
|
| 46 |
+
You rolled a 4 on a 6-sided die. The weather in Tokyo is sunny with a temperature of 12°C during fall.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
```
|
| 50 |
+
## Research Paper
|
| 51 |
+
|
| 52 |
+
[Read the research paper](agi/assets/ArtificialGeneralIntelligence.pdf)
|
| 53 |
+
|
| 54 |
+
This paper delineates a comprehensive architectural framework for the progressive realization of Artificial General Intelligence (AGI), predicated upon a modular-agentic paradigm. We present a system design that integrates sophisticated tool-use capabilities, hierarchical memory management, dynamic code execution, and nascent world-modeling functionalities. The proposed architecture, exemplified through a lightweight `Qwen3-4B-Instruct-2507-Q3_K_S.gguf` model, demonstrates a robust foundation for emergent cognitive properties such as autonomy, recursive self-improvement, and goal-oriented behavior. Furthermore, we explore the theoretical underpinnings of consciousness as an emergent property within complex neural architectures and postulate pathways towards super-intelligence through advanced computational and embodied interaction modalities. The exposition maintains a rigorous academic tone, employing advanced terminology to articulate the intricate conceptual and technical facets of AGI development.
|
| 55 |
+
|
agi/Qwen3-4B-Instruct-2507-Q3_K_S.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0ce20058cc0ed6b6c9213bb383589327e458c12ffce0842fc96867042d669c75
|
| 3 |
+
size 1886997600
|
agi/agent_config.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import List, Dict, Optional
|
| 3 |
+
|
| 4 |
+
@dataclass
|
| 5 |
+
class AgentConfig:
|
| 6 |
+
name: str = "Sophos Agent"
|
| 7 |
+
instructions: str = "You are an AI Agent."
|
| 8 |
+
model: str = "agi/Qwen3-4B-Instruct-2507-Q3_K_S.gguf"
|
| 9 |
+
max_iterations: int = 99
|
| 10 |
+
temperature: float = 0.7
|
| 11 |
+
max_tokens: int = 1050
|
| 12 |
+
verbose: bool = True
|
| 13 |
+
enable_memory: bool = False
|
| 14 |
+
memory_limit: int = 10
|
| 15 |
+
enable_tools: bool = True
|
| 16 |
+
allowed_tool_categories: Optional[List[str]] = None
|
| 17 |
+
sandbox_mode: bool = True
|
| 18 |
+
require_confirmation: bool = False
|
| 19 |
+
n_ctx: int = 2048
|
| 20 |
+
n_gpu_layers: int = 35
|
| 21 |
+
custom_instructions: str = ""
|
| 22 |
+
|
| 23 |
+
def to_dict(self) -> Dict:
|
| 24 |
+
return {
|
| 25 |
+
"name": self.name,
|
| 26 |
+
"instructions": self.instructions,
|
| 27 |
+
"model": self.model,
|
| 28 |
+
"max_iterations": self.max_iterations,
|
| 29 |
+
"temperature": self.temperature,
|
| 30 |
+
"max_tokens": self.max_tokens,
|
| 31 |
+
"verbose": self.verbose,
|
| 32 |
+
"enable_memory": self.enable_memory,
|
| 33 |
+
"memory_limit": self.memory_limit,
|
| 34 |
+
"enable_tools": self.enable_tools,
|
| 35 |
+
"allowed_tool_categories": self.allowed_tool_categories,
|
| 36 |
+
"sandbox_mode": self.sandbox_mode,
|
| 37 |
+
"require_confirmation": self.require_confirmation,
|
| 38 |
+
"n_ctx": self.n_ctx,
|
| 39 |
+
"n_gpu_layers": self.n_gpu_layers,
|
| 40 |
+
"custom_instructions": self.custom_instructions,
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
@classmethod
|
| 44 |
+
def from_dict(cls, config_dict: Dict) -> 'AgentConfig':
|
| 45 |
+
return cls(**config_dict)
|
| 46 |
+
|
| 47 |
+
def get_full_instructions(self) -> str:
|
| 48 |
+
base = self.instructions
|
| 49 |
+
if self.custom_instructions:
|
| 50 |
+
return f"{base}\n\n{self.custom_instructions}"
|
| 51 |
+
return base
|
| 52 |
+
|
| 53 |
+
class AgentPresets:
|
| 54 |
+
@staticmethod
|
| 55 |
+
def coding_assistant() -> AgentConfig:
|
| 56 |
+
return AgentConfig(
|
| 57 |
+
name="Code Assistant",
|
| 58 |
+
instructions="You are an expert coding assistant. Help with programming tasks, debugging, and code generation.",
|
| 59 |
+
max_tokens=1500,
|
| 60 |
+
allowed_tool_categories=["File Operations", "Text Processing", "Utilities"],
|
| 61 |
+
custom_instructions="Focus on clean, well-documented code."
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
@staticmethod
|
| 65 |
+
def data_analyst() -> AgentConfig:
|
| 66 |
+
return AgentConfig(
|
| 67 |
+
name="Data Analyst",
|
| 68 |
+
instructions="You are a data analyst. Help analyze data, perform calculations, and generate insights.",
|
| 69 |
+
allowed_tool_categories=["Mathematics", "Data Analysis", "Text Processing", "File Operations"]
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
@staticmethod
|
| 73 |
+
def general_assistant() -> AgentConfig:
|
| 74 |
+
return AgentConfig(
|
| 75 |
+
name="General Assistant",
|
| 76 |
+
instructions="You are a helpful AI assistant.",
|
| 77 |
+
allowed_tool_categories=None,
|
| 78 |
+
enable_memory=True,
|
| 79 |
+
memory_limit=10
|
| 80 |
+
)
|
agi/assets/AGI.png
ADDED
|
Git LFS Details
|
agi/assets/ArtificialGeneralIntelligence.pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7fb9e7d571936ff4cf2f36929eceedbfbfc0c88b601d50406a7e527472568549
|
| 3 |
+
size 785625
|
agi/assets/sophos.png
ADDED
|
Git LFS Details
|
agi/sophos.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agi.sophos_models import Sophos
|
| 2 |
+
from agi.sophos_tools import *
|
| 3 |
+
from agi.agent_config import AgentConfig
|
| 4 |
+
from typing import Optional, List, Dict
|
| 5 |
+
import threading
|
| 6 |
+
import time
|
| 7 |
+
import sys
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
class AgentMemory:
|
| 11 |
+
def __init__(self, limit: int = 10):
|
| 12 |
+
self.limit = limit
|
| 13 |
+
self.exchanges = []
|
| 14 |
+
|
| 15 |
+
def add_exchange(self, user_message: str, agent_response: str):
|
| 16 |
+
self.exchanges.append({"user": user_message, "agent": agent_response})
|
| 17 |
+
if len(self.exchanges) > self.limit:
|
| 18 |
+
self.exchanges = self.exchanges[-self.limit:]
|
| 19 |
+
|
| 20 |
+
def get_context(self) -> str:
|
| 21 |
+
if not self.exchanges:
|
| 22 |
+
return ""
|
| 23 |
+
context = "\nPrevious conversation:\n"
|
| 24 |
+
for i, exchange in enumerate(self.exchanges[-3:], 1):
|
| 25 |
+
context += f"[{i}] User: {exchange['user'][:100]}\nAgent: {exchange['agent'][:100]}\n"
|
| 26 |
+
return context
|
| 27 |
+
|
| 28 |
+
def clear(self):
|
| 29 |
+
self.exchanges = []
|
| 30 |
+
|
| 31 |
+
def to_dict(self) -> Dict:
|
| 32 |
+
return {"exchanges": self.exchanges, "limit": self.limit}
|
| 33 |
+
|
| 34 |
+
def from_dict(self, data: Dict):
|
| 35 |
+
self.exchanges = data.get("exchanges", [])
|
| 36 |
+
self.limit = data.get("limit", 10)
|
| 37 |
+
|
| 38 |
+
class Agent:
|
| 39 |
+
def __init__(self, name: str = None, instructions: str = None, model: str = None,
|
| 40 |
+
tools: list = None, config: Optional[AgentConfig] = None):
|
| 41 |
+
if config:
|
| 42 |
+
self.config = config
|
| 43 |
+
self.name = config.name
|
| 44 |
+
self.instructions = config.get_full_instructions()
|
| 45 |
+
self.model = config.model
|
| 46 |
+
else:
|
| 47 |
+
self.config = AgentConfig(
|
| 48 |
+
name=name or "Sophos Agent",
|
| 49 |
+
instructions=instructions or "You are an AI Agent.",
|
| 50 |
+
model=model or "agi/Qwen3-4B-Instruct-2507-Q3_K_S.gguf"
|
| 51 |
+
)
|
| 52 |
+
self.name = self.config.name
|
| 53 |
+
self.instructions = self.config.instructions
|
| 54 |
+
self.model = self.config.model
|
| 55 |
+
|
| 56 |
+
if tools:
|
| 57 |
+
self.tools = {tool.__name__: tool for tool in tools if hasattr(tool, "_is_tool")}
|
| 58 |
+
else:
|
| 59 |
+
self.tools = {}
|
| 60 |
+
|
| 61 |
+
self.memory = None
|
| 62 |
+
if self.config.enable_memory:
|
| 63 |
+
self.memory = AgentMemory(limit=self.config.memory_limit)
|
| 64 |
+
|
| 65 |
+
self.sophos = Sophos(
|
| 66 |
+
model_path=self.model,
|
| 67 |
+
n_ctx=self.config.n_ctx,
|
| 68 |
+
n_gpu_layers=self.config.n_gpu_layers
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
def process_tool_calls(self, response_content: str):
|
| 72 |
+
if "```tool" not in response_content:
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
tool_results = []
|
| 77 |
+
parts = response_content.split("```tool")
|
| 78 |
+
for i in range(1, len(parts)):
|
| 79 |
+
tool_block = parts[i].split("```")[0].strip()
|
| 80 |
+
tool_lines = [line.strip() for line in tool_block.split('\n') if line.strip()]
|
| 81 |
+
|
| 82 |
+
for tool_line in tool_lines:
|
| 83 |
+
if "(" in tool_line and ")" in tool_line:
|
| 84 |
+
tool_name = tool_line.split("(")[0].strip()
|
| 85 |
+
arg_part = tool_line.split("(", 1)[1].rsplit(")", 1)[0].strip()
|
| 86 |
+
|
| 87 |
+
if tool_name in self.tools:
|
| 88 |
+
# Print a red, attention-grabbing message when running a tool
|
| 89 |
+
try:
|
| 90 |
+
red = "\033[31m"
|
| 91 |
+
reset = "\033[0m"
|
| 92 |
+
# show the tool invocation in red
|
| 93 |
+
print(f"{red}running tool... {tool_name}({arg_part}){reset}")
|
| 94 |
+
except Exception:
|
| 95 |
+
# fallback to plain print if something odd happens
|
| 96 |
+
print(f"running tool... {tool_name}({arg_part})")
|
| 97 |
+
|
| 98 |
+
if not arg_part:
|
| 99 |
+
tool_result = self.tools[tool_name]()
|
| 100 |
+
else:
|
| 101 |
+
try:
|
| 102 |
+
if ',' in arg_part:
|
| 103 |
+
args = eval(f"({arg_part})")
|
| 104 |
+
if isinstance(args, tuple):
|
| 105 |
+
tool_result = self.tools[tool_name](*args)
|
| 106 |
+
else:
|
| 107 |
+
tool_result = self.tools[tool_name](args)
|
| 108 |
+
else:
|
| 109 |
+
parsed_arg = eval(arg_part)
|
| 110 |
+
tool_result = self.tools[tool_name](parsed_arg)
|
| 111 |
+
except Exception:
|
| 112 |
+
arg = arg_part.strip('"').strip("'")
|
| 113 |
+
tool_result = self.tools[tool_name](arg)
|
| 114 |
+
|
| 115 |
+
tool_results.append(f"{tool_name}: {tool_result}")
|
| 116 |
+
else:
|
| 117 |
+
tool_results.append(f"Error: Tool {tool_name} not found")
|
| 118 |
+
|
| 119 |
+
return tool_results if tool_results else None
|
| 120 |
+
except Exception:
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
def get_tools_names_and_descriptions(self):
|
| 124 |
+
return {name: func.__doc__ for name, func in self.tools.items()}
|
| 125 |
+
|
| 126 |
+
def get_stats(self) -> Dict:
|
| 127 |
+
return {
|
| 128 |
+
"name": self.name,
|
| 129 |
+
"model": self.model,
|
| 130 |
+
"tool_count": len(self.tools),
|
| 131 |
+
"memory_enabled": self.memory is not None
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
def save_state(self, filepath: str):
|
| 135 |
+
state = {
|
| 136 |
+
"config": self.config.to_dict(),
|
| 137 |
+
"memory": self.memory.to_dict() if self.memory else None
|
| 138 |
+
}
|
| 139 |
+
with open(filepath, 'w') as f:
|
| 140 |
+
json.dump(state, f, indent=2)
|
| 141 |
+
return f"State saved to {filepath}"
|
| 142 |
+
|
| 143 |
+
def load_state(self, filepath: str):
|
| 144 |
+
with open(filepath, 'r') as f:
|
| 145 |
+
state = json.load(f)
|
| 146 |
+
if state.get("memory") and self.memory:
|
| 147 |
+
self.memory.from_dict(state["memory"])
|
| 148 |
+
return f"State loaded from {filepath}"
|
| 149 |
+
|
| 150 |
+
def add_tools(self, tools: List):
|
| 151 |
+
for tool in tools:
|
| 152 |
+
if hasattr(tool, "_is_tool"):
|
| 153 |
+
self.tools[tool.__name__] = tool
|
| 154 |
+
return f"Added {len(tools)} tools"
|
| 155 |
+
|
| 156 |
+
def remove_tool(self, tool_name: str):
|
| 157 |
+
if tool_name in self.tools:
|
| 158 |
+
del self.tools[tool_name]
|
| 159 |
+
return f"Tool {tool_name} removed"
|
| 160 |
+
return f"Tool {tool_name} not found"
|
| 161 |
+
|
| 162 |
+
def list_tools(self) -> List[str]:
|
| 163 |
+
return list(self.tools.keys())
|
| 164 |
+
|
| 165 |
+
def run(self, prompt: str):
|
| 166 |
+
if getattr(self.config, 'verbose', False):
|
| 167 |
+
print(f"Running Agent: {self.name}, Model: {self.model}, Tools: {len(self.tools)}, Memory Enabled: {self.memory is not None}, prompt: {prompt[:50]}...")
|
| 168 |
+
memory_context = ""
|
| 169 |
+
if self.memory:
|
| 170 |
+
memory_context = self.memory.get_context()
|
| 171 |
+
|
| 172 |
+
system_prompt = f"""You are {self.name}. {self.instructions}
|
| 173 |
+
|
| 174 |
+
Tools: {self.get_tools_names_and_descriptions()}
|
| 175 |
+
|
| 176 |
+
Use tools in this format:
|
| 177 |
+
```tool
|
| 178 |
+
tool_name(arg)
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
WORKFLOW:
|
| 182 |
+
1. Use tools if needed for live info
|
| 183 |
+
2. Provide final response WITHOUT tool calls after getting results
|
| 184 |
+
3. Don't repeat tool calls
|
| 185 |
+
{memory_context}
|
| 186 |
+
|
| 187 |
+
user: {prompt}"""
|
| 188 |
+
|
| 189 |
+
messages = [
|
| 190 |
+
{"role": "system", "content": system_prompt},
|
| 191 |
+
{"role": "user", "content": prompt}
|
| 192 |
+
]
|
| 193 |
+
|
| 194 |
+
max_iterations = self.config.max_iterations
|
| 195 |
+
iteration = 0
|
| 196 |
+
last_tool_calls = []
|
| 197 |
+
final_response = None
|
| 198 |
+
|
| 199 |
+
while iteration < max_iterations:
|
| 200 |
+
iteration += 1
|
| 201 |
+
|
| 202 |
+
# show a simple "Thinking...." spinner while the model is generating
|
| 203 |
+
stop_event = threading.Event()
|
| 204 |
+
|
| 205 |
+
def _spinner(e: threading.Event):
|
| 206 |
+
dots = 0
|
| 207 |
+
try:
|
| 208 |
+
while not e.is_set():
|
| 209 |
+
print("\rThinking" + "." * (dots % 4) + " ", end="", flush=True)
|
| 210 |
+
dots += 1
|
| 211 |
+
time.sleep(0.5)
|
| 212 |
+
except Exception:
|
| 213 |
+
pass
|
| 214 |
+
# clear the line when done
|
| 215 |
+
try:
|
| 216 |
+
print("\r" + " " * 40 + "\r", end="", flush=True)
|
| 217 |
+
except Exception:
|
| 218 |
+
pass
|
| 219 |
+
|
| 220 |
+
spinner_thread = threading.Thread(target=_spinner, args=(stop_event,), daemon=True)
|
| 221 |
+
spinner_thread.start()
|
| 222 |
+
try:
|
| 223 |
+
response = self.sophos.ask(str(messages),
|
| 224 |
+
max_tokens=self.config.max_tokens,
|
| 225 |
+
temperature=self.config.temperature)
|
| 226 |
+
finally:
|
| 227 |
+
stop_event.set()
|
| 228 |
+
spinner_thread.join(timeout=0.2)
|
| 229 |
+
|
| 230 |
+
current_response = response
|
| 231 |
+
tool_results = self.process_tool_calls(current_response)
|
| 232 |
+
|
| 233 |
+
if tool_results is None:
|
| 234 |
+
final_response = current_response
|
| 235 |
+
break
|
| 236 |
+
|
| 237 |
+
all_results = "\n".join(tool_results)
|
| 238 |
+
current_tool_calls = [result.split(":")[0] for result in tool_results]
|
| 239 |
+
|
| 240 |
+
if current_tool_calls == last_tool_calls and iteration > 1:
|
| 241 |
+
messages.append({"role": "assistant", "content": current_response})
|
| 242 |
+
messages.append({"role": "user", "content": f"Results:\n{all_results}\n\nProvide final answer without more tools."})
|
| 243 |
+
else:
|
| 244 |
+
messages.append({"role": "assistant", "content": current_response})
|
| 245 |
+
messages.append({"role": "user", "content": f"Results:\n{all_results}\n\nProvide final response."})
|
| 246 |
+
|
| 247 |
+
last_tool_calls = current_tool_calls
|
| 248 |
+
|
| 249 |
+
if iteration >= max_iterations:
|
| 250 |
+
final_response = current_response
|
| 251 |
+
|
| 252 |
+
if self.memory and final_response:
|
| 253 |
+
self.memory.add_exchange(prompt, final_response)
|
| 254 |
+
|
| 255 |
+
return final_response or current_response
|
agi/sophos_models.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import contextlib
|
| 2 |
+
import os
|
| 3 |
+
from llama_cpp import Llama
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Sophos:
|
| 7 |
+
def __init__(self, model_path="agi/Qwen3-4B-Instruct-2507-Q3_K_S.gguf", n_ctx=2048, n_gpu_layers=35):
|
| 8 |
+
"""Initialize the Llama model while suppressing any noisy stdout/stderr from the underlying library."""
|
| 9 |
+
self.model_path = model_path
|
| 10 |
+
self.n_ctx = n_ctx
|
| 11 |
+
self.n_gpu_layers = n_gpu_layers
|
| 12 |
+
# suppress any prints coming from llama_cpp during initialization
|
| 13 |
+
with open(os.devnull, 'w') as devnull:
|
| 14 |
+
with contextlib.redirect_stdout(devnull), contextlib.redirect_stderr(devnull):
|
| 15 |
+
self.model = Llama(model_path=model_path, n_ctx=n_ctx, n_gpu_layers=n_gpu_layers)
|
| 16 |
+
|
| 17 |
+
def ask(self, prompt, max_tokens=1050, temperature=0.7):
|
| 18 |
+
"""Call the model while suppressing any noisy stdout/stderr from the underlying library."""
|
| 19 |
+
messages = [{"role": "user", "content": prompt}]
|
| 20 |
+
# suppress any prints coming from llama_cpp while creating completion
|
| 21 |
+
with open(os.devnull, 'w') as devnull:
|
| 22 |
+
with contextlib.redirect_stdout(devnull), contextlib.redirect_stderr(devnull):
|
| 23 |
+
output = self.model.create_chat_completion(messages, max_tokens=max_tokens, temperature=temperature)
|
| 24 |
+
|
| 25 |
+
return output["choices"][0]["message"]["content"]
|
| 26 |
+
|
| 27 |
+
if __name__ == "__main__":
|
| 28 |
+
sophos = Sophos()
|
| 29 |
+
while True:
|
| 30 |
+
prompt = input("\nPrompt: ")
|
| 31 |
+
if prompt.lower() in ['exit', 'quit', 'q']:
|
| 32 |
+
break
|
| 33 |
+
print(sophos.ask(prompt))
|
agi/sophos_tools.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import random
|
| 3 |
+
import string
|
| 4 |
+
import math
|
| 5 |
+
import os
|
| 6 |
+
import base64
|
| 7 |
+
import hashlib
|
| 8 |
+
import time
|
| 9 |
+
import json
|
| 10 |
+
import re
|
| 11 |
+
from typing import List
|
| 12 |
+
|
| 13 |
+
def function_tool(func):
|
| 14 |
+
func._is_tool = True
|
| 15 |
+
return func
|
| 16 |
+
|
| 17 |
+
@function_tool
|
| 18 |
+
def get_weather(city: str) -> str:
|
| 19 |
+
'''fetches weather information for a given city.'''
|
| 20 |
+
month = datetime.datetime.now().month
|
| 21 |
+
season_temps = {(12, 1, 2): ("winter", 2), (3, 4, 5): ("spring", 15), (6, 7, 8): ("summer", 28)}
|
| 22 |
+
season, base = next((v for k, v in season_temps.items() if month in k), ("fall", 12))
|
| 23 |
+
return f"Weather in {city}: sunny, {base + random.randint(-5, 5)}°C, {season}"
|
| 24 |
+
|
| 25 |
+
@function_tool
|
| 26 |
+
def get_time() -> str:
|
| 27 |
+
'''fetches the current time.'''
|
| 28 |
+
return f"Current time: {datetime.datetime.now().strftime('%H:%M:%S')}"
|
| 29 |
+
|
| 30 |
+
@function_tool
|
| 31 |
+
def get_date() -> str:
|
| 32 |
+
'''fetches the current date.'''
|
| 33 |
+
return f"Date: {datetime.datetime.now().strftime('%Y-%m-%d')}"
|
| 34 |
+
|
| 35 |
+
@function_tool
|
| 36 |
+
def get_day() -> str:
|
| 37 |
+
'''fetches the current day of the week.'''
|
| 38 |
+
return f"Today is {datetime.datetime.now().strftime('%A')}"
|
| 39 |
+
|
| 40 |
+
@function_tool
|
| 41 |
+
def calculate(expression: str) -> str:
|
| 42 |
+
'''performs basic arithmetic calculations.'''
|
| 43 |
+
try:
|
| 44 |
+
return f"{expression} = {eval(expression, {'__builtins__': None}, {})}"
|
| 45 |
+
except Exception as e:
|
| 46 |
+
return f"Error: {str(e)}"
|
| 47 |
+
|
| 48 |
+
@function_tool
|
| 49 |
+
def generate_password(length: int = 12) -> str:
|
| 50 |
+
'''Generates a random password of specified length.'''
|
| 51 |
+
if length < 8:
|
| 52 |
+
return "Error: min 8 chars"
|
| 53 |
+
return f"Password: {''.join(random.choice(string.ascii_letters + string.digits + string.punctuation) for _ in range(length))}"
|
| 54 |
+
|
| 55 |
+
@function_tool
|
| 56 |
+
def convert_temperature(value: float, from_unit: str, to_unit: str) -> str:
|
| 57 |
+
'''Converts temperature between Celsius, Fahrenheit, and Kelvin.'''
|
| 58 |
+
conversions = {
|
| 59 |
+
('celsius', 'fahrenheit'): lambda v: (v * 9/5) + 32,
|
| 60 |
+
('celsius', 'kelvin'): lambda v: v + 273.15,
|
| 61 |
+
('fahrenheit', 'celsius'): lambda v: (v - 32) * 5/9,
|
| 62 |
+
('fahrenheit', 'kelvin'): lambda v: (v - 32) * 5/9 + 273.15,
|
| 63 |
+
('kelvin', 'celsius'): lambda v: v - 273.15,
|
| 64 |
+
('kelvin', 'fahrenheit'): lambda v: (v - 273.15) * 9/5 + 32
|
| 65 |
+
}
|
| 66 |
+
key = (from_unit.lower(), to_unit.lower())
|
| 67 |
+
return f"{value}° {from_unit} = {conversions[key](value):.2f}° {to_unit}" if key in conversions else "Error: invalid units"
|
| 68 |
+
|
| 69 |
+
@function_tool
|
| 70 |
+
def roll_dice(sides: int, rolls: int = 1) -> str:
|
| 71 |
+
'''Rolls a dice with specified sides a given number of times.'''
|
| 72 |
+
results = [random.randint(1, sides) for _ in range(rolls)]
|
| 73 |
+
return f"{rolls}d{sides}: {results}, Total: {sum(results)}"
|
| 74 |
+
|
| 75 |
+
@function_tool
|
| 76 |
+
def word_count(text: str) -> str:
|
| 77 |
+
'''Counts the number of words in the given text.'''
|
| 78 |
+
return f"Words: {len(text.split())}"
|
| 79 |
+
|
| 80 |
+
@function_tool
|
| 81 |
+
def get_factorial(number: int) -> str:
|
| 82 |
+
'''Calculates the factorial of a given number.'''
|
| 83 |
+
try:
|
| 84 |
+
return f"{number}! = {math.factorial(number)}"
|
| 85 |
+
except:
|
| 86 |
+
return "Error: invalid input"
|
| 87 |
+
|
| 88 |
+
@function_tool
|
| 89 |
+
def list_sort(numbers: List[float], ascending: bool = True) -> str:
|
| 90 |
+
'''Sorts a list of numbers in ascending or descending order.'''
|
| 91 |
+
return f"Sorted: {sorted(numbers, reverse=not ascending)}"
|
| 92 |
+
|
| 93 |
+
@function_tool
|
| 94 |
+
def generate_uuid() -> str:
|
| 95 |
+
'''Generates a random UUID.'''
|
| 96 |
+
import uuid
|
| 97 |
+
return f"UUID: {uuid.uuid4()}"
|
| 98 |
+
|
| 99 |
+
@function_tool
|
| 100 |
+
def save_file(file_path: str, content: str) -> str:
|
| 101 |
+
'''Saves content to a specified file path. use as save_file("filename.extension", "content") .for new lines use \\n'''
|
| 102 |
+
folder = "agi/sandbox/agent_sandbox/"
|
| 103 |
+
os.makedirs(folder, exist_ok=True)
|
| 104 |
+
try:
|
| 105 |
+
with open(folder + file_path, 'w') as f:
|
| 106 |
+
f.write(content)
|
| 107 |
+
return f"Saved to {folder + file_path}"
|
| 108 |
+
except Exception as e:
|
| 109 |
+
return f"Error: {str(e)}"
|
| 110 |
+
|
| 111 |
+
@function_tool
|
| 112 |
+
def read_file(file_path: str) -> str:
|
| 113 |
+
'''Reads content from a specified file path in the sandbox.'''
|
| 114 |
+
try:
|
| 115 |
+
with open("agi/sandbox/agent_sandbox/" + file_path, 'r') as f:
|
| 116 |
+
return f"Content:\n{f.read()}"
|
| 117 |
+
except Exception as e:
|
| 118 |
+
return f"Error: {str(e)}"
|
| 119 |
+
|
| 120 |
+
@function_tool
|
| 121 |
+
def list_files() -> str:
|
| 122 |
+
'''Lists all files in the agent sandbox directory.'''
|
| 123 |
+
folder = "agi/sandbox/agent_sandbox/"
|
| 124 |
+
return f"Files: {', '.join(os.listdir(folder))}" if os.path.exists(folder) and os.listdir(folder) else "Empty"
|
| 125 |
+
|
| 126 |
+
@function_tool
|
| 127 |
+
def delete_file(file_path: str) -> str:
|
| 128 |
+
'''Deletes a file from the agent sandbox.'''
|
| 129 |
+
try:
|
| 130 |
+
os.remove("agi/sandbox/agent_sandbox/" + file_path)
|
| 131 |
+
return f"Deleted {file_path}"
|
| 132 |
+
except Exception as e:
|
| 133 |
+
return f"Error: {str(e)}"
|
| 134 |
+
|
| 135 |
+
@function_tool
|
| 136 |
+
def append_to_file(file_path: str, content: str) -> str:
|
| 137 |
+
'''Appends content to an existing file in the sandbox.'''
|
| 138 |
+
folder = "agi/sandbox/agent_sandbox/"
|
| 139 |
+
os.makedirs(folder, exist_ok=True)
|
| 140 |
+
try:
|
| 141 |
+
with open(folder + file_path, 'a') as f:
|
| 142 |
+
f.write(content)
|
| 143 |
+
return f"Appended to {file_path}"
|
| 144 |
+
except Exception as e:
|
| 145 |
+
return f"Error: {str(e)}"
|
| 146 |
+
|
| 147 |
+
@function_tool
|
| 148 |
+
def search_text(text: str, pattern: str) -> str:
|
| 149 |
+
'''Searches for a pattern in text and returns matches.'''
|
| 150 |
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
| 151 |
+
return f"Found {len(matches)} matches: {matches[:10]}" if matches else f"No matches for: {pattern}"
|
| 152 |
+
|
| 153 |
+
@function_tool
|
| 154 |
+
def replace_text(text: str, old: str, new: str) -> str:
|
| 155 |
+
'''Replaces occurrences of old text with new text.'''
|
| 156 |
+
return f"Replaced {text.count(old)} occurrences: {text.replace(old, new)}"
|
| 157 |
+
|
| 158 |
+
@function_tool
|
| 159 |
+
def encode_base64(text: str) -> str:
|
| 160 |
+
'''Encodes text to base64.'''
|
| 161 |
+
return f"Base64: {base64.b64encode(text.encode()).decode()}"
|
| 162 |
+
|
| 163 |
+
@function_tool
|
| 164 |
+
def decode_base64(encoded: str) -> str:
|
| 165 |
+
'''Decodes base64 text.'''
|
| 166 |
+
try:
|
| 167 |
+
return f"Decoded: {base64.b64decode(encoded.encode()).decode()}"
|
| 168 |
+
except Exception as e:
|
| 169 |
+
return f"Error: {str(e)}"
|
| 170 |
+
|
| 171 |
+
@function_tool
|
| 172 |
+
def hash_text(text: str, algorithm: str = "sha256") -> str:
|
| 173 |
+
'''Generates hash of text using specified algorithm (md5, sha1, sha256, sha512).'''
|
| 174 |
+
algos = {"md5": hashlib.md5, "sha1": hashlib.sha1, "sha256": hashlib.sha256, "sha512": hashlib.sha512}
|
| 175 |
+
return f"{algorithm}: {algos[algorithm](text.encode()).hexdigest()}" if algorithm in algos else "Error: invalid algorithm"
|
| 176 |
+
|
| 177 |
+
@function_tool
|
| 178 |
+
def generate_random_number(min_val: int, max_val: int) -> str:
|
| 179 |
+
'''Generates a random number between min and max values.'''
|
| 180 |
+
return f"Random: {random.randint(min_val, max_val)}"
|
| 181 |
+
|
| 182 |
+
@function_tool
|
| 183 |
+
def get_list_statistics(numbers: List[float]) -> str:
|
| 184 |
+
'''Calculates statistics (mean, median, min, max, sum) for a list of numbers.'''
|
| 185 |
+
import statistics
|
| 186 |
+
return f"Mean: {statistics.mean(numbers):.2f}, Median: {statistics.median(numbers):.2f}, Min: {min(numbers)}, Max: {max(numbers)}, Sum: {sum(numbers)}"
|
| 187 |
+
|
| 188 |
+
@function_tool
|
| 189 |
+
def reverse_text(text: str) -> str:
|
| 190 |
+
'''Reverses the given text.'''
|
| 191 |
+
return f"Reversed: {text[::-1]}"
|
| 192 |
+
|
| 193 |
+
@function_tool
|
| 194 |
+
def to_uppercase(text: str) -> str:
|
| 195 |
+
'''Converts text to uppercase.'''
|
| 196 |
+
return f"Uppercase: {text.upper()}"
|
| 197 |
+
|
| 198 |
+
@function_tool
|
| 199 |
+
def to_lowercase(text: str) -> str:
|
| 200 |
+
'''Converts text to lowercase.'''
|
| 201 |
+
return f"Lowercase: {text.lower()}"
|
| 202 |
+
|
| 203 |
+
@function_tool
|
| 204 |
+
def count_characters(text: str, include_spaces: bool = True) -> str:
|
| 205 |
+
'''Counts characters in text.'''
|
| 206 |
+
count = len(text) if include_spaces else len(text.replace(" ", ""))
|
| 207 |
+
return f"Chars: {count}"
|
| 208 |
+
|
| 209 |
+
@function_tool
|
| 210 |
+
def json_format(data: str) -> str:
|
| 211 |
+
'''Formats a JSON string with proper indentation.'''
|
| 212 |
+
try:
|
| 213 |
+
return f"JSON:\n{json.dumps(json.loads(data), indent=2)}"
|
| 214 |
+
except Exception as e:
|
| 215 |
+
return f"Error: {str(e)}"
|
| 216 |
+
|
| 217 |
+
@function_tool
|
| 218 |
+
def get_timestamp() -> str:
|
| 219 |
+
'''Returns current Unix timestamp.'''
|
| 220 |
+
return f"Timestamp: {int(time.time())}"
|
| 221 |
+
|
| 222 |
+
@function_tool
|
| 223 |
+
def convert_timestamp(timestamp: int) -> str:
|
| 224 |
+
'''Converts Unix timestamp to readable date/time.'''
|
| 225 |
+
return f"{timestamp} = {datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')}"
|
| 226 |
+
|
| 227 |
+
@function_tool
|
| 228 |
+
def calculate_age(birth_year: int) -> str:
|
| 229 |
+
'''Calculates age based on birth year.'''
|
| 230 |
+
return f"Age: {datetime.datetime.now().year - birth_year} years"
|
| 231 |
+
|
| 232 |
+
@function_tool
|
| 233 |
+
def days_until(target_date: str) -> str:
|
| 234 |
+
'''Calculates days until a target date (format: YYYY-MM-DD).'''
|
| 235 |
+
try:
|
| 236 |
+
delta = datetime.datetime.strptime(target_date, "%Y-%m-%d") - datetime.datetime.now()
|
| 237 |
+
return f"Days until {target_date}: {delta.days}"
|
| 238 |
+
except Exception as e:
|
| 239 |
+
return f"Error: {str(e)}"
|
| 240 |
+
|
| 241 |
+
@function_tool
|
| 242 |
+
def create_acronym(phrase: str) -> str:
|
| 243 |
+
'''Creates an acronym from a phrase.'''
|
| 244 |
+
return f"Acronym: {''.join([w[0].upper() for w in phrase.split() if w])}"
|
| 245 |
+
|
| 246 |
+
@function_tool
|
| 247 |
+
def fibonacci(n: int) -> str:
|
| 248 |
+
'''Generates Fibonacci sequence up to n terms.'''
|
| 249 |
+
if n <= 0 or n > 50:
|
| 250 |
+
return "Error: n must be 1-50"
|
| 251 |
+
fib = [0, 1]
|
| 252 |
+
for i in range(2, n):
|
| 253 |
+
fib.append(fib[i-1] + fib[i-2])
|
| 254 |
+
return f"Fibonacci({n}): {fib[:n]}"
|
| 255 |
+
|
| 256 |
+
@function_tool
|
| 257 |
+
def is_prime(number: int) -> str:
|
| 258 |
+
'''Checks if a number is prime.'''
|
| 259 |
+
if number < 2:
|
| 260 |
+
return f"{number} is not prime"
|
| 261 |
+
for i in range(2, int(number ** 0.5) + 1):
|
| 262 |
+
if number % i == 0:
|
| 263 |
+
return f"{number} is not prime"
|
| 264 |
+
return f"{number} is prime"
|
| 265 |
+
|
| 266 |
+
@function_tool
|
| 267 |
+
def gcd(a: int, b: int) -> str:
|
| 268 |
+
'''Calculates the greatest common divisor of two numbers.'''
|
| 269 |
+
return f"GCD({a}, {b}) = {math.gcd(a, b)}"
|
| 270 |
+
|
| 271 |
+
@function_tool
|
| 272 |
+
def lcm(a: int, b: int) -> str:
|
| 273 |
+
'''Calculates the least common multiple of two numbers.'''
|
| 274 |
+
return f"LCM({a}, {b}) = {abs(a * b) // math.gcd(a, b)}"
|
| 275 |
+
|
| 276 |
+
def all_tools():
|
| 277 |
+
"""Returns a list of all functions decorated with @function_tool."""
|
| 278 |
+
return [obj for obj in globals().values() if callable(obj) and hasattr(obj, '_is_tool') and obj._is_tool]
|
agi/tool_categories.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agi.sophos_tools import *
|
| 2 |
+
from typing import List, Dict
|
| 3 |
+
|
| 4 |
+
class ToolCategory:
|
| 5 |
+
def __init__(self, name: str, description: str, tools: List):
|
| 6 |
+
self.name = name
|
| 7 |
+
self.description = description
|
| 8 |
+
self.tools = tools
|
| 9 |
+
|
| 10 |
+
def get_tools(self):
|
| 11 |
+
return self.tools
|
| 12 |
+
|
| 13 |
+
def __repr__(self):
|
| 14 |
+
return f"ToolCategory('{self.name}', {len(self.tools)} tools)"
|
| 15 |
+
|
| 16 |
+
TIME_TOOLS = ToolCategory("Time & Date", "Time and date tools",
|
| 17 |
+
[get_time, get_date, get_day, get_timestamp, convert_timestamp, calculate_age, days_until])
|
| 18 |
+
|
| 19 |
+
MATH_TOOLS = ToolCategory("Mathematics", "Math operations",
|
| 20 |
+
[calculate, get_factorial, fibonacci, is_prime, gcd, lcm, generate_random_number, roll_dice])
|
| 21 |
+
|
| 22 |
+
FILE_TOOLS = ToolCategory("File Operations", "File management in sandbox",
|
| 23 |
+
[save_file, read_file, list_files, delete_file, append_to_file])
|
| 24 |
+
|
| 25 |
+
TEXT_TOOLS = ToolCategory("Text Processing", "Text manipulation",
|
| 26 |
+
[word_count, reverse_text, to_uppercase, to_lowercase, count_characters, search_text, replace_text, create_acronym])
|
| 27 |
+
|
| 28 |
+
DATA_TOOLS = ToolCategory("Data Analysis", "Data processing",
|
| 29 |
+
[list_sort, get_list_statistics])
|
| 30 |
+
|
| 31 |
+
ENCODING_TOOLS = ToolCategory("Encoding & Hashing", "Encoding and hashing",
|
| 32 |
+
[encode_base64, decode_base64, hash_text])
|
| 33 |
+
|
| 34 |
+
UTILITY_TOOLS = ToolCategory("Utilities", "General utilities",
|
| 35 |
+
[get_weather, generate_password, convert_temperature, generate_uuid, json_format])
|
| 36 |
+
|
| 37 |
+
ALL_CATEGORIES = [TIME_TOOLS, MATH_TOOLS, FILE_TOOLS, TEXT_TOOLS, DATA_TOOLS, ENCODING_TOOLS, UTILITY_TOOLS]
|
| 38 |
+
|
| 39 |
+
def get_tools_by_category(category_name: str) -> List:
|
| 40 |
+
for category in ALL_CATEGORIES:
|
| 41 |
+
if category.name.lower() == category_name.lower():
|
| 42 |
+
return category.get_tools()
|
| 43 |
+
return []
|
| 44 |
+
|
| 45 |
+
def get_tools_by_categories(category_names: List[str]) -> List:
|
| 46 |
+
tools = []
|
| 47 |
+
for name in category_names:
|
| 48 |
+
tools.extend(get_tools_by_category(name))
|
| 49 |
+
return tools
|
| 50 |
+
|
| 51 |
+
def get_all_categorized_tools() -> List:
|
| 52 |
+
tools = []
|
| 53 |
+
for category in ALL_CATEGORIES:
|
| 54 |
+
tools.extend(category.get_tools())
|
| 55 |
+
return tools
|
| 56 |
+
|
| 57 |
+
def list_categories() -> Dict[str, str]:
|
| 58 |
+
return {cat.name: cat.description for cat in ALL_CATEGORIES}
|
| 59 |
+
|
| 60 |
+
def get_category_info(category_name: str) -> Dict:
|
| 61 |
+
for category in ALL_CATEGORIES:
|
| 62 |
+
if category.name.lower() == category_name.lower():
|
| 63 |
+
return {
|
| 64 |
+
"name": category.name,
|
| 65 |
+
"description": category.description,
|
| 66 |
+
"tool_count": len(category.tools),
|
| 67 |
+
"tools": [tool.__name__ for tool in category.tools]
|
| 68 |
+
}
|
| 69 |
+
return {}
|
agi_gui.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
Simple chat GUI for the project using customtkinter.
|
| 4 |
+
|
| 5 |
+
Features:
|
| 6 |
+
- Chat history area
|
| 7 |
+
- Input field + Send button
|
| 8 |
+
- Clear history button
|
| 9 |
+
- Status label to show when the agent is thinking
|
| 10 |
+
- Uses a background thread to call agent.run so the UI stays responsive
|
| 11 |
+
|
| 12 |
+
Usage: python agi_gui.py
|
| 13 |
+
|
| 14 |
+
This file expects the project-local Agent and AgentConfig to exist at
|
| 15 |
+
`agi.sophos` and `agi.agent_config`, and `all_tools` at `agi.sophos_tools`.
|
| 16 |
+
If `customtkinter` is not installed, the UI will show a helpful error.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import threading
|
| 20 |
+
import queue
|
| 21 |
+
import time
|
| 22 |
+
import traceback
|
| 23 |
+
try:
|
| 24 |
+
import customtkinter as ctk
|
| 25 |
+
except Exception:
|
| 26 |
+
ctk = None
|
| 27 |
+
|
| 28 |
+
from datetime import datetime
|
| 29 |
+
|
| 30 |
+
if ctk:
|
| 31 |
+
ctk.set_appearance_mode("System")
|
| 32 |
+
ctk.set_default_color_theme("blue")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def format_message(role: str, text: str) -> str:
|
| 36 |
+
ts = datetime.now().strftime("%H:%M:%S")
|
| 37 |
+
return f"[{ts}] {role}: {text}\n\n"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class AGIChatApp:
|
| 41 |
+
def __init__(self, root):
|
| 42 |
+
self.root = root
|
| 43 |
+
self.root.title("AGI Chat")
|
| 44 |
+
self.root.geometry("800x600")
|
| 45 |
+
|
| 46 |
+
# message queue for thread -> UI communication
|
| 47 |
+
self._q = queue.Queue()
|
| 48 |
+
|
| 49 |
+
# top frame: history
|
| 50 |
+
self.history = ctk.CTkTextbox(master=root, width=760, height=420)
|
| 51 |
+
self.history.configure(state="disabled")
|
| 52 |
+
self.history.pack(padx=20, pady=(20, 6))
|
| 53 |
+
|
| 54 |
+
# status and controls
|
| 55 |
+
ctrl_frame = ctk.CTkFrame(master=root)
|
| 56 |
+
ctrl_frame.pack(fill="x", padx=20, pady=(0, 6))
|
| 57 |
+
|
| 58 |
+
self.status_label = ctk.CTkLabel(master=ctrl_frame, text="Ready")
|
| 59 |
+
self.status_label.pack(side="left", padx=(8, 6))
|
| 60 |
+
|
| 61 |
+
clear_btn = ctk.CTkButton(master=ctrl_frame, text="Clear", width=80, command=self.clear_history)
|
| 62 |
+
clear_btn.pack(side="right", padx=(6, 8))
|
| 63 |
+
|
| 64 |
+
# bottom frame: entry and send
|
| 65 |
+
bottom = ctk.CTkFrame(master=root)
|
| 66 |
+
bottom.pack(fill="x", padx=20, pady=(0, 20))
|
| 67 |
+
|
| 68 |
+
self.entry = ctk.CTkEntry(master=bottom, placeholder_text="Type your message here...")
|
| 69 |
+
self.entry.pack(side="left", fill="x", expand=True, padx=(6, 6), pady=6)
|
| 70 |
+
self.entry.bind("<Return>", self._on_enter)
|
| 71 |
+
|
| 72 |
+
send_btn = ctk.CTkButton(master=bottom, text="Send", width=120, command=self.on_send)
|
| 73 |
+
send_btn.pack(side="right", padx=(6, 6), pady=6)
|
| 74 |
+
|
| 75 |
+
# Agent will be lazy-initialized on first send to avoid long startup at UI open
|
| 76 |
+
self.agent = None
|
| 77 |
+
self.agent_lock = threading.Lock()
|
| 78 |
+
|
| 79 |
+
# periodically poll the queue for results from worker threads
|
| 80 |
+
self.root.after(100, self._poll_queue)
|
| 81 |
+
|
| 82 |
+
def _append_history(self, text: str):
|
| 83 |
+
self.history.configure(state="normal")
|
| 84 |
+
self.history.insert("end", text)
|
| 85 |
+
self.history.see("end")
|
| 86 |
+
self.history.configure(state="disabled")
|
| 87 |
+
|
| 88 |
+
def clear_history(self):
|
| 89 |
+
self.history.configure(state="normal")
|
| 90 |
+
self.history.delete("0.0", "end")
|
| 91 |
+
self.history.configure(state="disabled")
|
| 92 |
+
self.status_label.configure(text="Cleared")
|
| 93 |
+
|
| 94 |
+
def _on_enter(self, event):
|
| 95 |
+
# prevent the default newline insertion
|
| 96 |
+
self.on_send()
|
| 97 |
+
return "break"
|
| 98 |
+
|
| 99 |
+
def on_send(self):
|
| 100 |
+
user_text = self.entry.get().strip()
|
| 101 |
+
if not user_text:
|
| 102 |
+
return
|
| 103 |
+
self.entry.delete(0, "end")
|
| 104 |
+
self._append_history(format_message("User", user_text))
|
| 105 |
+
self.status_label.configure(text="Thinking...")
|
| 106 |
+
|
| 107 |
+
# start background thread to call agent
|
| 108 |
+
t = threading.Thread(target=self._background_agent_call, args=(user_text,), daemon=True)
|
| 109 |
+
t.start()
|
| 110 |
+
|
| 111 |
+
def _init_agent(self):
|
| 112 |
+
# only run import and initialization once, in guarded block
|
| 113 |
+
if self.agent is not None:
|
| 114 |
+
return
|
| 115 |
+
try:
|
| 116 |
+
# local project imports
|
| 117 |
+
from agi.sophos import Agent
|
| 118 |
+
from agi.agent_config import AgentConfig
|
| 119 |
+
from agi.sophos_tools import all_tools
|
| 120 |
+
|
| 121 |
+
config = AgentConfig(
|
| 122 |
+
name="Sophos-GUI",
|
| 123 |
+
instructions="You are an AI agent.",
|
| 124 |
+
max_iterations=30,
|
| 125 |
+
max_tokens=1500,
|
| 126 |
+
temperature=0.7,
|
| 127 |
+
verbose=False,
|
| 128 |
+
enable_memory=True,
|
| 129 |
+
memory_limit=5,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
toolbox = all_tools()
|
| 133 |
+
# instantiate Agent
|
| 134 |
+
self.agent = Agent(config=config, tools=toolbox)
|
| 135 |
+
self._append_history(format_message("System", "Agent initialized."))
|
| 136 |
+
except Exception as e:
|
| 137 |
+
tb = traceback.format_exc()
|
| 138 |
+
self._append_history(format_message("Error", f"Failed to initialize agent: {e}\n{tb}"))
|
| 139 |
+
self.status_label.configure(text="Agent init failed")
|
| 140 |
+
|
| 141 |
+
def _background_agent_call(self, prompt: str):
|
| 142 |
+
# ensure agent is ready
|
| 143 |
+
with self.agent_lock:
|
| 144 |
+
if self.agent is None:
|
| 145 |
+
self._init_agent()
|
| 146 |
+
|
| 147 |
+
if self.agent is None:
|
| 148 |
+
# initialization failed
|
| 149 |
+
self._q.put(("error", "Agent not available"))
|
| 150 |
+
return
|
| 151 |
+
|
| 152 |
+
try:
|
| 153 |
+
start = time.time()
|
| 154 |
+
# call agent.run which may be blocking
|
| 155 |
+
resp = self.agent.run(prompt)
|
| 156 |
+
elapsed = time.time() - start
|
| 157 |
+
self._q.put(("response", resp, elapsed))
|
| 158 |
+
except Exception as e:
|
| 159 |
+
tb = traceback.format_exc()
|
| 160 |
+
self._q.put(("error", f"{e}\n{tb}"))
|
| 161 |
+
|
| 162 |
+
def _poll_queue(self):
|
| 163 |
+
try:
|
| 164 |
+
while True:
|
| 165 |
+
item = self._q.get_nowait()
|
| 166 |
+
if not item:
|
| 167 |
+
continue
|
| 168 |
+
kind = item[0]
|
| 169 |
+
if kind == "response":
|
| 170 |
+
_, resp, elapsed = item
|
| 171 |
+
self._append_history(format_message("Agent", resp))
|
| 172 |
+
self.status_label.configure(text=f"Done ({elapsed:.1f}s)")
|
| 173 |
+
elif kind == "error":
|
| 174 |
+
_, err = item
|
| 175 |
+
self._append_history(format_message("Error", str(err)))
|
| 176 |
+
self.status_label.configure(text="Error")
|
| 177 |
+
else:
|
| 178 |
+
# unknown message
|
| 179 |
+
self._append_history(format_message("Debug", str(item)))
|
| 180 |
+
except queue.Empty:
|
| 181 |
+
pass
|
| 182 |
+
finally:
|
| 183 |
+
self.root.after(100, self._poll_queue)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def main():
|
| 187 |
+
if ctk is None:
|
| 188 |
+
print("customtkinter is not installed. Install it with: pip install customtkinter")
|
| 189 |
+
# Fallback to a basic tkinter popup that informs the user
|
| 190 |
+
try:
|
| 191 |
+
import tkinter as tk
|
| 192 |
+
from tkinter import messagebox
|
| 193 |
+
root = tk.Tk()
|
| 194 |
+
root.withdraw()
|
| 195 |
+
messagebox.showerror("Missing dependency", "customtkinter is not installed.\nRun: pip install customtkinter")
|
| 196 |
+
except Exception:
|
| 197 |
+
pass
|
| 198 |
+
return
|
| 199 |
+
|
| 200 |
+
root = ctk.CTk()
|
| 201 |
+
app = AGIChatApp(root)
|
| 202 |
+
root.mainloop()
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
if __name__ == "__main__":
|
| 206 |
+
main()
|
main.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agi.sophos import Agent
|
| 2 |
+
from agi.sophos_tools import all_tools, function_tool
|
| 3 |
+
from agi.agent_config import AgentConfig
|
| 4 |
+
|
| 5 |
+
config = AgentConfig(
|
| 6 |
+
name="Sophos",
|
| 7 |
+
instructions="You are an AI agent.",
|
| 8 |
+
max_iterations=30,
|
| 9 |
+
max_tokens=1500,
|
| 10 |
+
temperature=0.7,
|
| 11 |
+
verbose=True,
|
| 12 |
+
enable_memory=True,
|
| 13 |
+
memory_limit=5
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
toolbox = all_tools()
|
| 17 |
+
agent = Agent(config=config, tools=toolbox)
|
| 18 |
+
|
| 19 |
+
response = agent.run("roll a dice. also whats the weather like today? then write a short poem about it.")
|
| 20 |
+
print("\nAgent response:\n", response)
|
simple_test.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agi.sophos import Agent
|
| 2 |
+
from agi.sophos_tools import *
|
| 3 |
+
|
| 4 |
+
toolbox = all_tools()
|
| 5 |
+
|
| 6 |
+
agent = Agent(
|
| 7 |
+
name="Sophos Agent",
|
| 8 |
+
instructions="You are an AI Agent.",
|
| 9 |
+
model="agi/Qwen3-4B-Instruct-2507-Q3_K_S.gguf",
|
| 10 |
+
tools=toolbox,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
prompt = "Roll a dice, also whats the weather in Tokyo?"
|
| 14 |
+
response = agent.run(prompt)
|
| 15 |
+
print(response)
|