Skip to content

Commit b6237a9

Browse files
committed
Add README instructions and CI
1 parent 23b854d commit b6237a9

File tree

17 files changed

+250
-7
lines changed

17 files changed

+250
-7
lines changed

.github/workflows/lint.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Unit-&-Lint
2+
on:
3+
push:
4+
paths: ["agent/**", "pyproject.toml"]
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- uses: actions/setup-python@v5
11+
with:
12+
python-version: "3.10"
13+
- run: pip install -e .[dev]
14+
- run: python -m pytest -q

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "externals/llama.cpp"]
2+
path = externals/llama.cpp
3+
url = https://github.com/ggerganov/llama.cpp

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,3 +765,16 @@ If you use this code, please cite:
765765
## License
766766

767767
MIT License. See [LICENSE](LICENSE) for details.
768+
769+
## 🔥 Local Qwen 3 Agent (Apple Silicon)
770+
771+
> New in `qwen3-agent` branch
772+
773+
1. `brew install cmake rust`
774+
2. `git submodule update --init`
775+
3. `bash scripts/build_llama_metal.sh`
776+
4. `pip install -e .`
777+
5. Download **Qwen3-14B-Q4_K_M.gguf**`~/models`
778+
6. `bash scripts/run_agent.sh`
779+
780+
The `run_agent.sh` script exports the config path and launches `python -m agent.main`. Users on Intel Macs can unset `LLAMA_METAL` to force CPU inference.

agent/__init__.py

Whitespace-only changes.

agent/config/agent_config.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[model]
2+
# path to the local GGUF model
3+
gguf_path = "~/models/Qwen3-14B-Q4_K_M.gguf"
4+
context_length = 4096
5+
temperature = 0.2
6+
top_p = 0.95
7+
top_k = 40
8+
enable_thinking = true
9+
10+
[performance]
11+
threads = 8
12+
gpu_layers = 1
13+
14+
[tools]
15+
code_interpreter = true
16+
shell = false
17+
search = false
18+
19+
[paths]
20+
python_tool = "python"
21+
search_api_key = ""

agent/evolution/nsga.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from pymoo.algorithms.moo.nsga2 import NSGA2
2+
from pymoo.factory import get_sampling, get_crossover, get_mutation
3+
from pymoo.optimize import minimize
4+
5+
from agent.tools import run_code_variant, CODE_TESTS
6+
7+
8+
def evolve(initial_code: str, pop_size: int = 8, gens: int = 6):
9+
def fitness(code: str):
10+
ok, runtime = run_code_variant(code, tests=CODE_TESTS)
11+
return [-int(ok), runtime]
12+
13+
# Placeholder for population creation and NSGA-II execution
14+
algo = NSGA2(
15+
pop_size=pop_size,
16+
sampling=get_sampling("int_random"),
17+
crossover=get_crossover("int_sbx"),
18+
mutation=get_mutation("int_pm"),
19+
)
20+
# Additional code needed to define the Problem and run minimize
21+
# TODO: implement full evolution loop

agent/main.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import tomllib
2+
import time
3+
import os
4+
from pathlib import Path
5+
from llama_cpp import Llama
6+
from qwen_agent import Assistant
7+
from agent.tools import build_tools
8+
9+
CFG_PATH = os.environ.get("AGENT_CONFIG", Path(__file__).with_suffix('.toml'))
10+
CFG = tomllib.loads(Path(CFG_PATH).read_text())
11+
12+
os.environ["OMP_NUM_THREADS"] = str(CFG["performance"]["threads"])
13+
14+
llm = Llama(
15+
model_path=CFG["model"]["gguf_path"],
16+
n_ctx=CFG["model"]["context_length"],
17+
n_gpu_layers=CFG["performance"]["gpu_layers"],
18+
temperature=CFG["model"]["temperature"],
19+
top_p=CFG["model"]["top_p"],
20+
top_k=CFG["model"]["top_k"],
21+
logits_all=False,
22+
)
23+
24+
assistant = Assistant(
25+
llm=llm,
26+
function_list=build_tools(CFG["tools"], CFG.get("paths", {})),
27+
enable_thinking=CFG["model"]["enable_thinking"],
28+
)
29+
30+
def chat(prompt: str) -> str:
31+
start = time.perf_counter()
32+
rsp = assistant.chat(prompt)["content"]
33+
print(f"[{(time.perf_counter()-start):.2f}s]")
34+
return rsp
35+
36+
if __name__ == "__main__":
37+
while True:
38+
try:
39+
print(chat(input(">>> ")))
40+
except (EOFError, KeyboardInterrupt):
41+
break

agent/tools/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from typing import Dict, List
2+
3+
from .code_interpreter import CodeInterpreterTool
4+
from .shell import ShellTool
5+
from .fetch import FetchTool
6+
7+
8+
def build_tools(cfg: Dict[str, bool], paths: Dict[str, str]) -> List[dict]:
9+
tools = []
10+
if cfg.get("code_interpreter"):
11+
tools.append(CodeInterpreterTool(paths.get("python_tool", "python")))
12+
if cfg.get("shell"):
13+
tools.append(ShellTool())
14+
if cfg.get("search"):
15+
tools.append(FetchTool(paths.get("search_api_key", "")))
16+
return [t.as_mcp() for t in tools]
17+
18+
CODE_TESTS: str = """def test_add():\n assert add(2,2) == 4\n"""
19+
20+
21+
def run_code_variant(code: str, tests: str = CODE_TESTS):
22+
full_code = code + "\n" + tests + "\nif __name__ == '__main__':\n import pytest, sys; sys.exit(pytest.main(['-q']))\n"
23+
tool = CodeInterpreterTool()
24+
output = tool.run(full_code)
25+
success = '1 passed' in output
26+
return success, 0.0

agent/tools/code_interpreter.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import subprocess
2+
import tempfile
3+
from pathlib import Path
4+
5+
class CodeInterpreterTool:
6+
def __init__(self, python_path: str = "python"):
7+
self.python_path = python_path
8+
9+
def as_mcp(self) -> dict:
10+
return {
11+
"name": "code_interpreter",
12+
"description": "Execute Python code in a sandboxed environment",
13+
"parameters": {"type": "string", "name": "code"},
14+
"call": self.run,
15+
}
16+
17+
def run(self, code: str) -> str:
18+
with tempfile.TemporaryDirectory() as tmp:
19+
p = subprocess.run(
20+
[self.python_path, "-I", "-S", "-"],
21+
input=code.encode(),
22+
stdout=subprocess.PIPE,
23+
stderr=subprocess.STDOUT,
24+
cwd=tmp,
25+
timeout=10,
26+
)
27+
return p.stdout.decode()

agent/tools/fetch.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import requests
2+
3+
class FetchTool:
4+
def __init__(self, api_key: str = ""):
5+
self.api_key = api_key
6+
7+
def as_mcp(self) -> dict:
8+
return {
9+
"name": "search",
10+
"description": "Fetch a web page or search result",
11+
"parameters": {"type": "string", "name": "query"},
12+
"call": self.run,
13+
}
14+
15+
def run(self, query: str) -> str:
16+
if not self.api_key:
17+
return "offline"
18+
r = requests.get("https://api.duckduckgo.com", params={"q": query, "format": "json"}, timeout=5)
19+
data = r.json()
20+
return data.get("Abstract", "")

0 commit comments

Comments
 (0)