racoogent 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- racoogent-0.1.0/LICENSE +21 -0
- racoogent-0.1.0/PKG-INFO +15 -0
- racoogent-0.1.0/README.md +34 -0
- racoogent-0.1.0/pyproject.toml +7 -0
- racoogent-0.1.0/racoogent/__init__.py +1 -0
- racoogent-0.1.0/racoogent/cli.py +164 -0
- racoogent-0.1.0/racoogent/commands/__init__.py +0 -0
- racoogent-0.1.0/racoogent/commands/first_setup.py +81 -0
- racoogent-0.1.0/racoogent/config.py +17 -0
- racoogent-0.1.0/racoogent/interconnection/__init__.py +1 -0
- racoogent-0.1.0/racoogent/interconnection/clawlink.py +48 -0
- racoogent-0.1.0/racoogent/interconnection/mcp_manager.py +42 -0
- racoogent-0.1.0/racoogent/interconnection/sandbox.py +15 -0
- racoogent-0.1.0/racoogent/interconnection/service.py +43 -0
- racoogent-0.1.0/racoogent/interconnection/tools.py +4 -0
- racoogent-0.1.0/racoogent/llm/__init__.py +1 -0
- racoogent-0.1.0/racoogent/llm/alibaba_provider.py +12 -0
- racoogent-0.1.0/racoogent/llm/base.py +4 -0
- racoogent-0.1.0/racoogent/llm/deepseek_provider.py +10 -0
- racoogent-0.1.0/racoogent/llm/factory.py +12 -0
- racoogent-0.1.0/racoogent/llm/gemini_provider.py +8 -0
- racoogent-0.1.0/racoogent/llm/llama_provider.py +27 -0
- racoogent-0.1.0/racoogent/llm/openai_provider.py +11 -0
- racoogent-0.1.0/racoogent/skills/__init__.py +0 -0
- racoogent-0.1.0/racoogent/skills/chat_ui.py +41 -0
- racoogent-0.1.0/racoogent/skills/manager.py +16 -0
- racoogent-0.1.0/racoogent/skills/restoration.py +12 -0
- racoogent-0.1.0/racoogent/skills/tui.py +10 -0
- racoogent-0.1.0/racoogent.egg-info/PKG-INFO +15 -0
- racoogent-0.1.0/racoogent.egg-info/SOURCES.txt +33 -0
- racoogent-0.1.0/racoogent.egg-info/dependency_links.txt +1 -0
- racoogent-0.1.0/racoogent.egg-info/entry_points.txt +2 -0
- racoogent-0.1.0/racoogent.egg-info/requires.txt +9 -0
- racoogent-0.1.0/racoogent.egg-info/top_level.txt +1 -0
- racoogent-0.1.0/setup.cfg +4 -0
racoogent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Doojimin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
racoogent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: racoogent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Dist: python-dotenv
|
|
7
|
+
Requires-Dist: pyyaml
|
|
8
|
+
Requires-Dist: rich
|
|
9
|
+
Requires-Dist: typer
|
|
10
|
+
Requires-Dist: pydantic
|
|
11
|
+
Requires-Dist: httpx
|
|
12
|
+
Requires-Dist: openai
|
|
13
|
+
Requires-Dist: google-generativeai
|
|
14
|
+
Requires-Dist: litellm
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# racoogent 🦝
|
|
2
|
+
|
|
3
|
+
**A local-first AI agent that lives on your Linux**
|
|
4
|
+
|
|
5
|
+
Your data stays on your machine. Racoogent connects powerful cloud LLMs to your local system through a secure interconnection service.
|
|
6
|
+
|
|
7
|
+
### Why racoogent?
|
|
8
|
+
Most AI agents live in the cloud. Racoogent lives on *your* Linux. You control the sandbox, the files, and the tools.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
- 🔌 **Interconnection Service** - LLM <-> System hub
|
|
12
|
+
- 🧠 **Multi-LLM** - openai, gemini, deepseek, llama (Ollama Cloud), alibaba
|
|
13
|
+
- 🛠️ **Tools** - run_shell, read_file with sandboxed execution
|
|
14
|
+
- 🔗 **MCP** - Connect any MCP server
|
|
15
|
+
- 💳 **Clawlink** - Premium connectors
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
```bash
|
|
19
|
+
pip install racoogent
|
|
20
|
+
racoogent first-setup
|
|
21
|
+
racoogent chat
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
```bash
|
|
26
|
+
racoogent chat
|
|
27
|
+
racoogent models
|
|
28
|
+
racoogent use gpt-oss:120b
|
|
29
|
+
racoogent mcp list
|
|
30
|
+
racoogent clawlink status
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__="0.1.0"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import typer, os, httpx, json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from racoogent.config import load_config
|
|
5
|
+
from racoogent.interconnection import InterconnectionService
|
|
6
|
+
from racoogent.llm.factory import get_llm
|
|
7
|
+
from racoogent.commands.first_setup import run as run_first_setup
|
|
8
|
+
load_dotenv()
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
|
|
11
|
+
def _maybe_show_tui():
|
|
12
|
+
import sys
|
|
13
|
+
if "skill" in sys.argv: return
|
|
14
|
+
try:
|
|
15
|
+
from racoogent.skills.manager import is_enabled
|
|
16
|
+
from racoogent.skills.tui import show as show_tui
|
|
17
|
+
if is_enabled("tui"): show_tui()
|
|
18
|
+
except: pass
|
|
19
|
+
|
|
20
|
+
@app.callback()
|
|
21
|
+
def _root(): _maybe_show_tui()
|
|
22
|
+
|
|
23
|
+
def detect_provider():
|
|
24
|
+
if os.getenv("LLAMA_API_KEY"): return "llama"
|
|
25
|
+
if os.getenv("OPENAI_API_KEY"): return "openai"
|
|
26
|
+
if os.getenv("GEMINI_API_KEY"): return "gemini"
|
|
27
|
+
return "ollama"
|
|
28
|
+
|
|
29
|
+
def update_env_model(m):
|
|
30
|
+
p=Path(".env"); lines=p.read_text().splitlines() if p.exists() else []; new=[]; found=False
|
|
31
|
+
for l in lines:
|
|
32
|
+
if l.startswith("LLAMA_MODEL="): new.append(f"LLAMA_MODEL={m}"); found=True
|
|
33
|
+
else: new.append(l)
|
|
34
|
+
if not found: new.append(f"LLAMA_MODEL={m}")
|
|
35
|
+
p.write_text("\n".join(new)+"\n")
|
|
36
|
+
|
|
37
|
+
def fetch_models():
|
|
38
|
+
base=os.getenv("LLAMA_BASE_URL","https://ollama.com/v1"); key=os.getenv("LLAMA_API_KEY")
|
|
39
|
+
if not key: return []
|
|
40
|
+
try:
|
|
41
|
+
r=httpx.get(base.rstrip("/")+"/models", headers={"Authorization": f"Bearer {key}"}, timeout=10)
|
|
42
|
+
if r.status_code==200 and "data" in r.json(): return [x["id"] for x in r.json()["data"]]
|
|
43
|
+
except: pass
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
@app.command()
|
|
47
|
+
def first_setup(): run_first_setup()
|
|
48
|
+
@app.command()
|
|
49
|
+
def models():
|
|
50
|
+
ms=fetch_models(); curr=os.getenv("LLAMA_MODEL")
|
|
51
|
+
for i,m in enumerate(ms,1): print(f"({i}) {m}{' <-- current' if m==curr else ''}")
|
|
52
|
+
@app.command()
|
|
53
|
+
def use(model: str): update_env_model(model); print(f"✅ {model}")
|
|
54
|
+
@app.command()
|
|
55
|
+
def mcp(action: str=typer.Argument(...), name: str=typer.Argument(None), command: str=typer.Argument(None), token_key: str=typer.Argument(None)):
|
|
56
|
+
cfg=Path.home()/".config"/"racoogent"/"mcp_servers.json"; cfg.parent.mkdir(parents=True,exist_ok=True)
|
|
57
|
+
if not cfg.exists(): cfg.write_text("{}")
|
|
58
|
+
data=json.loads(cfg.read_text())
|
|
59
|
+
if action=="list":
|
|
60
|
+
if not data: print("No MCP")
|
|
61
|
+
else:
|
|
62
|
+
for k,v in data.items(): print(f"- {k}: {v.get('command') if isinstance(v,dict) else v}")
|
|
63
|
+
elif action=="add":
|
|
64
|
+
if not name or not command: print('add <name> "<cmd>"'); return
|
|
65
|
+
if token_key and "=" in token_key: k,v=token_key.split("=",1); data[name]={"command":command,"env":{k:v}}
|
|
66
|
+
else: data[name]=command
|
|
67
|
+
cfg.write_text(json.dumps(data,indent=2)); print(f"✅ {name}")
|
|
68
|
+
elif action=="add-token":
|
|
69
|
+
if "=" not in token_key: return
|
|
70
|
+
k,v=token_key.split("=",1); ex=data.get(name,""); ex={"command":ex,"env":{}} if isinstance(ex,str) else ex; ex.setdefault("env",{})[k]=v; data[name]=ex; cfg.write_text(json.dumps(data,indent=2)); print(f"✅ {k}")
|
|
71
|
+
elif action=="remove": data.pop(name,None); cfg.write_text(json.dumps(data,indent=2))
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def skill(name: str=typer.Argument(...), action: str=typer.Argument("status"), value: str=typer.Argument(None)):
|
|
75
|
+
from racoogent.skills.manager import set_enabled, is_enabled
|
|
76
|
+
from racoogent.skills.restoration import CHECKPOINT
|
|
77
|
+
import subprocess, json
|
|
78
|
+
n=name.lower()
|
|
79
|
+
def to_bool(s): return s.lower() in ["true","1","on","yes"] if s else True
|
|
80
|
+
if n in ["tui","every-logs"]:
|
|
81
|
+
if n=="every-logs" and action=="view":
|
|
82
|
+
p=Path.home()/".config"/"racoogent"/"execution.log"
|
|
83
|
+
if p.exists(): print(p.read_text()[-2000:])
|
|
84
|
+
else: print("No logs"); return
|
|
85
|
+
if action=="enable":
|
|
86
|
+
b=False if (value and value.lower() in ["false","0","off","no"]) else (to_bool(value) if value else True)
|
|
87
|
+
elif action=="disable": b=False
|
|
88
|
+
elif action in ["true","on"]: b=True
|
|
89
|
+
elif action in ["false","off"]: b=False
|
|
90
|
+
elif action=="status": print(f"{n}: {is_enabled(n)}"); return
|
|
91
|
+
else: print("usage"); return
|
|
92
|
+
set_enabled(n,b); print(f"{'✅' if b else '🛑'} {n} {'enabled' if b else 'disabled'}")
|
|
93
|
+
elif n in ["stop-and-restoration","stop"]:
|
|
94
|
+
if CHECKPOINT.exists(): print(json.loads(CHECKPOINT.read_text())["timestamp"])
|
|
95
|
+
else: print("No checkpoint")
|
|
96
|
+
|
|
97
|
+
@app.command()
|
|
98
|
+
def chat(provider: str=None, model: str=None):
|
|
99
|
+
from racoogent.skills.manager import is_enabled, load as load_skills
|
|
100
|
+
from racoogent.skills.restoration import save_checkpoint, CHECKPOINT
|
|
101
|
+
from racoogent.skills.chat_ui import console, show_welcome, print_agent, print_user, input_box, print_tool
|
|
102
|
+
import sys, json
|
|
103
|
+
restored=None
|
|
104
|
+
if CHECKPOINT.exists():
|
|
105
|
+
d=json.loads(CHECKPOINT.read_text())
|
|
106
|
+
console.print(f"[yellow]💾 checkpoint {d['timestamp']}[/]")
|
|
107
|
+
if console.input("Restore? [y/N]: ").lower()=="y": restored=d["messages"]
|
|
108
|
+
else: CHECKPOINT.unlink(missing_ok=True)
|
|
109
|
+
|
|
110
|
+
if provider is None: provider=detect_provider()
|
|
111
|
+
final=model or os.getenv("LLAMA_MODEL")
|
|
112
|
+
|
|
113
|
+
mcp_path=Path.home()/".config"/"racoogent"/"mcp_servers.json"
|
|
114
|
+
mcp_count=len(json.loads(mcp_path.read_text())) if mcp_path.exists() else 0
|
|
115
|
+
skills_enabled=[k for k,v in load_skills().items() if v]
|
|
116
|
+
show_welcome(provider, final, mcp_count, skills_enabled)
|
|
117
|
+
|
|
118
|
+
# ★ racoogent가 먼저 HI!
|
|
119
|
+
print_agent("HI! 👋 I'm **racoogent** - your local-first Linux agent! 🦝\n\nWhat can I help you with today?")
|
|
120
|
+
|
|
121
|
+
config=load_config(); ic=InterconnectionService(config); llm=get_llm(provider, final)
|
|
122
|
+
messages=restored if restored else [{"role":"system","content":"You are racoogent, friendly raccoon agent"}]
|
|
123
|
+
|
|
124
|
+
orig=ic.execute_tool
|
|
125
|
+
def logged(a,b):
|
|
126
|
+
if is_enabled("every-logs"): print_tool(a,b)
|
|
127
|
+
return orig(a,b)
|
|
128
|
+
ic.execute_tool=logged
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
while True:
|
|
132
|
+
# ★ 네가 치는건 밑에 네모 박스
|
|
133
|
+
u = input_box()
|
|
134
|
+
if not u: continue
|
|
135
|
+
if u.strip()=="/bye": console.print("[dim]Bye! 👋[/]"); break
|
|
136
|
+
if u.strip()=="/clear":
|
|
137
|
+
console.clear(); show_welcome(provider, final, mcp_count, skills_enabled)
|
|
138
|
+
print_agent("HI! 👋 Ready again!")
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# 네가 친건 파란 박스에
|
|
142
|
+
print_user(u)
|
|
143
|
+
messages.append({"role":"user","content":u})
|
|
144
|
+
|
|
145
|
+
with console.status("[magenta]racoogent is typing...[/]", spinner="dots"):
|
|
146
|
+
resp=llm.chat(messages, tools=ic.get_tool_definitions())
|
|
147
|
+
|
|
148
|
+
# 얘가 말한건 마젠타 박스에
|
|
149
|
+
print_agent(resp["content"])
|
|
150
|
+
|
|
151
|
+
while resp.get("tool_calls"):
|
|
152
|
+
for tc in resp["tool_calls"]:
|
|
153
|
+
print_tool(tc["name"], tc["arguments"])
|
|
154
|
+
out=ic.execute_tool(tc["name"], tc["arguments"])
|
|
155
|
+
messages.append({"role":"tool","tool_call_id":tc["id"],"content":str(out)})
|
|
156
|
+
with console.status("[yellow]Executing tools...[/]", spinner="bouncingBar"):
|
|
157
|
+
resp=llm.chat(messages, tools=ic.get_tool_definitions())
|
|
158
|
+
print_agent(resp["content"])
|
|
159
|
+
messages.append({"role":"assistant","content":resp["content"]})
|
|
160
|
+
|
|
161
|
+
except KeyboardInterrupt:
|
|
162
|
+
console.print("\n[yellow]⏸️ Saving...[/]"); save_checkpoint(messages); sys.exit(0)
|
|
163
|
+
|
|
164
|
+
if __name__=="__main__": app()
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
try:
|
|
4
|
+
import httpx
|
|
5
|
+
except ImportError:
|
|
6
|
+
httpx = None
|
|
7
|
+
|
|
8
|
+
PROVIDERS = {
|
|
9
|
+
"1": {"id": "openai", "name": "OpenAI API", "key": "OPENAI_API_KEY", "url": "https://platform.openai.com/api-keys"},
|
|
10
|
+
"2": {"id": "llama", "name": "Llama API (Ollama Cloud / Together.ai)", "key": "LLAMA_API_KEY", "url": "https://ollama.com/settings/keys"},
|
|
11
|
+
"3": {"id": "gemini", "name": "Gemini API", "key": "GEMINI_API_KEY", "url": "https://aistudio.google.com/apikey"},
|
|
12
|
+
"4": {"id": "deepseek", "name": "DeepSeek API", "key": "DEEPSEEK_API_KEY", "url": "https://platform.deepseek.com/api_keys"},
|
|
13
|
+
"5": {"id": "alibaba", "name": "Alibaba Cloud API", "key": "ALIBABA_API_KEY", "url": "https://modelstudio.console.alibabacloud.com/"},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def fetch_ollama_models(base_url, api_key):
|
|
17
|
+
if not httpx:
|
|
18
|
+
return []
|
|
19
|
+
models = []
|
|
20
|
+
try:
|
|
21
|
+
url = base_url.rstrip("/") + "/models"
|
|
22
|
+
r = httpx.get(url, headers={"Authorization": f"Bearer {api_key}"}, timeout=10)
|
|
23
|
+
if r.status_code == 200:
|
|
24
|
+
data = r.json()
|
|
25
|
+
if "data" in data:
|
|
26
|
+
models = [m["id"] for m in data["data"]]
|
|
27
|
+
elif "models" in data:
|
|
28
|
+
models = [m["name"] for m in data["models"]]
|
|
29
|
+
if not models:
|
|
30
|
+
r2 = httpx.get(base_url.rstrip("/").replace("/v1","") + "/api/tags", headers={"Authorization": f"Bearer {api_key}"}, timeout=10)
|
|
31
|
+
if r2.status_code == 200:
|
|
32
|
+
models = [m["name"] for m in r2.json().get("models",[])]
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"(Failed to fetch models: {e})")
|
|
35
|
+
return sorted(models)
|
|
36
|
+
|
|
37
|
+
def run():
|
|
38
|
+
print("\n=== RacooGent First Setup ===\n")
|
|
39
|
+
print("Select LLM API")
|
|
40
|
+
for num, p in PROVIDERS.items():
|
|
41
|
+
print(f"({num}) {p['name']}")
|
|
42
|
+
choice = input("\nEnter number (e.g. 2 or 1,2,3): > ").strip()
|
|
43
|
+
env_path = Path(".env")
|
|
44
|
+
env_data = {}
|
|
45
|
+
if env_path.exists():
|
|
46
|
+
with open(env_path) as f:
|
|
47
|
+
for line in f:
|
|
48
|
+
if "=" in line and not line.startswith("#"):
|
|
49
|
+
k,v = line.strip().split("=",1)
|
|
50
|
+
env_data[k]=v
|
|
51
|
+
for c in [x.strip() for x in choice.split(",")]:
|
|
52
|
+
if c not in PROVIDERS: continue
|
|
53
|
+
p = PROVIDERS[c]
|
|
54
|
+
print(f"\n[{p['id']}] Get key from: {p['url']}")
|
|
55
|
+
api_key = input(f"[{p['id']}] API Key: > ").strip()
|
|
56
|
+
if not api_key: continue
|
|
57
|
+
env_data[p['key']] = api_key
|
|
58
|
+
if p['id'] == 'llama':
|
|
59
|
+
base = input(f"[{p['id']}] Base URL [https://ollama.com/v1]: > ").strip()
|
|
60
|
+
base = base if base else 'https://ollama.com/v1'
|
|
61
|
+
env_data['LLAMA_BASE_URL'] = base
|
|
62
|
+
print(f"[{p['id']}] 모델 목록 불러오는 중... ({base})")
|
|
63
|
+
models = fetch_ollama_models(base, api_key)
|
|
64
|
+
if models:
|
|
65
|
+
print(f"\n[{p['id']}] Models available ({len(models)}개):")
|
|
66
|
+
for i, m in enumerate(models, 1):
|
|
67
|
+
print(f"({i}) {m}")
|
|
68
|
+
sel = input(f"\nSelect (1-{len(models)}) or 직접 입력: > ").strip()
|
|
69
|
+
if sel.isdigit() and 1 <= int(sel) <= len(models):
|
|
70
|
+
env_data['LLAMA_MODEL'] = models[int(sel)-1]
|
|
71
|
+
elif sel:
|
|
72
|
+
env_data['LLAMA_MODEL'] = sel
|
|
73
|
+
else:
|
|
74
|
+
env_data['LLAMA_MODEL'] = models[0]
|
|
75
|
+
else:
|
|
76
|
+
model = input(f"[{p['id']}] Model [gpt-oss:20b]: > ").strip()
|
|
77
|
+
env_data['LLAMA_MODEL'] = model if model else 'gpt-oss:20b'
|
|
78
|
+
with open(env_path, 'w') as f:
|
|
79
|
+
for k,v in env_data.items():
|
|
80
|
+
f.write(f"{k}={v}\n")
|
|
81
|
+
print("\n.env saved! ✅")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os, yaml, pathlib
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
load_dotenv()
|
|
4
|
+
CONFIG_PATH = pathlib.Path.home() / ".config" / "racoogent" / "config.yaml"
|
|
5
|
+
DEFAULT_CONFIG = {
|
|
6
|
+
"llm": {"provider": "openai","model":"gpt-4o-mini"},
|
|
7
|
+
"interconnection": {"allow_shell": True, "allow_file_write": True, "sandbox_dir": "~/racoogent_sandbox"}
|
|
8
|
+
}
|
|
9
|
+
def load_config():
|
|
10
|
+
if CONFIG_PATH.exists():
|
|
11
|
+
return yaml.safe_load(open(CONFIG_PATH))
|
|
12
|
+
return DEFAULT_CONFIG
|
|
13
|
+
def init_config():
|
|
14
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
if not CONFIG_PATH.exists():
|
|
16
|
+
yaml.dump(DEFAULT_CONFIG, open(CONFIG_PATH,"w"), allow_unicode=True)
|
|
17
|
+
print(f"Config created at {CONFIG_PATH}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .service import InterconnectionService
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
import os, httpx, json, pathlib
|
|
3
|
+
CONFIG = pathlib.Path.home() / ".config" / "racoogent" / "clawlink.json"
|
|
4
|
+
|
|
5
|
+
def get_creds():
|
|
6
|
+
return {
|
|
7
|
+
"api_key": os.getenv("CLAWLINK_API_KEY"),
|
|
8
|
+
"base_url": os.getenv("CLAWLINK_API_URL", "https://api.clawlink.ai/v1")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
def save_config(data):
|
|
12
|
+
CONFIG.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
open(CONFIG,"w").write(json.dumps(data, indent=2))
|
|
14
|
+
|
|
15
|
+
def list_skills():
|
|
16
|
+
creds = get_creds()
|
|
17
|
+
if not creds["api_key"]:
|
|
18
|
+
return {"error": "CLAWLINK_API_KEY not set in.env"}
|
|
19
|
+
try:
|
|
20
|
+
r = httpx.get(f"{creds['base_url']}/skills", headers={"Authorization": f"Bearer {creds['api_key']}"}, timeout=10)
|
|
21
|
+
return r.json()
|
|
22
|
+
except Exception as e:
|
|
23
|
+
return {"error": str(e), "hint": "Check CLAWLINK_API_URL"}
|
|
24
|
+
|
|
25
|
+
def call_skill(skill_name, args={}):
|
|
26
|
+
creds = get_creds()
|
|
27
|
+
try:
|
|
28
|
+
r = httpx.post(f"{creds['base_url']}/skills/{skill_name}/call", headers={"Authorization": f"Bearer {creds['api_key']}"}, json=args, timeout=30)
|
|
29
|
+
return r.json()
|
|
30
|
+
except Exception as e:
|
|
31
|
+
return {"error": str(e)}
|
|
32
|
+
|
|
33
|
+
def get_clawlink_tools():
|
|
34
|
+
# 동적으로 skill을 tool로 변환
|
|
35
|
+
data = list_skills()
|
|
36
|
+
if "error" in data or "skills" not in data:
|
|
37
|
+
return []
|
|
38
|
+
tools = []
|
|
39
|
+
for s in data.get("skills", []):
|
|
40
|
+
tools.append({
|
|
41
|
+
"type":"function",
|
|
42
|
+
"function":{
|
|
43
|
+
"name": f"clawlink_{s['name']}",
|
|
44
|
+
"description": f"Clawlink skill: {s.get('description','')} (paid)",
|
|
45
|
+
"parameters": s.get("parameters", {"type":"object","properties":{}})
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
return tools
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
import json, pathlib, subprocess, os
|
|
3
|
+
CONFIG = pathlib.Path.home() / ".config" / "racoogent" / "mcp_servers.json"
|
|
4
|
+
|
|
5
|
+
def load_servers():
|
|
6
|
+
if not CONFIG.exists():
|
|
7
|
+
return {}
|
|
8
|
+
try:
|
|
9
|
+
return json.loads(open(CONFIG).read())
|
|
10
|
+
except:
|
|
11
|
+
return {}
|
|
12
|
+
|
|
13
|
+
def save_servers(data):
|
|
14
|
+
CONFIG.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
open(CONFIG,"w").write(json.dumps(data, indent=2))
|
|
16
|
+
|
|
17
|
+
def add_server(name, command):
|
|
18
|
+
servers = load_servers()
|
|
19
|
+
servers[name] = {"command": command, "enabled": True}
|
|
20
|
+
save_servers(servers)
|
|
21
|
+
return servers
|
|
22
|
+
|
|
23
|
+
def list_servers():
|
|
24
|
+
return load_servers()
|
|
25
|
+
|
|
26
|
+
def get_mcp_tools():
|
|
27
|
+
# MCP는 실제로 stdio로 연결해서 tools/list 해야하지만, 지금은 스텁으로 등록
|
|
28
|
+
# 나중에 pip install mcp 하면 실제 연결
|
|
29
|
+
servers = load_servers()
|
|
30
|
+
tools = []
|
|
31
|
+
for name, cfg in servers.items():
|
|
32
|
+
if not cfg.get("enabled"): continue
|
|
33
|
+
# 각 MCP 서버가 제공하는 툴을 OpenAI function 형태로 변환한다고 가정
|
|
34
|
+
tools.append({
|
|
35
|
+
"type":"function",
|
|
36
|
+
"function":{
|
|
37
|
+
"name": f"mcp_{name}_call",
|
|
38
|
+
"description": f"MCP server {name}: {cfg['command']} - tools will be dynamically fetched",
|
|
39
|
+
"parameters": {"type":"object","properties":{"tool":{"type":"string"},"arguments":{"type":"object"}},"required":["tool"]}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
return tools
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import pathlib, subprocess
|
|
2
|
+
class Sandbox:
|
|
3
|
+
def __init__(self, sandbox_dir="~/racoogent_sandbox", allow_shell=True, allow_write=True):
|
|
4
|
+
self.root = pathlib.Path(sandbox_dir).expanduser()
|
|
5
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
6
|
+
self.allow_shell = allow_shell
|
|
7
|
+
def run_shell(self, command: str) -> str:
|
|
8
|
+
if "rm -rf /" in command: return "Blocked"
|
|
9
|
+
try:
|
|
10
|
+
r = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=15)
|
|
11
|
+
return r.stdout + r.stderr
|
|
12
|
+
except Exception as e: return str(e)
|
|
13
|
+
def read_file(self, path: str) -> str:
|
|
14
|
+
try: return pathlib.Path(path).expanduser().read_text()[:10000]
|
|
15
|
+
except Exception as e: return str(e)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
from.sandbox import Sandbox
|
|
3
|
+
from.tools import TOOLS
|
|
4
|
+
from.mcp_manager import get_mcp_tools
|
|
5
|
+
from.clawlink import get_clawlink_tools, call_skill
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
class InterconnectionService:
|
|
9
|
+
def __init__(self, config):
|
|
10
|
+
ic = config.get("interconnection", {})
|
|
11
|
+
self.sandbox = Sandbox(ic.get("sandbox_dir","~/racoogent_sandbox"))
|
|
12
|
+
self.base_tools = TOOLS
|
|
13
|
+
|
|
14
|
+
def get_tool_definitions(self):
|
|
15
|
+
tools = list(self.base_tools)
|
|
16
|
+
try:
|
|
17
|
+
tools.extend(get_mcp_tools())
|
|
18
|
+
except Exception as e:
|
|
19
|
+
print(f"[mcp] load fail: {e}")
|
|
20
|
+
try:
|
|
21
|
+
tools.extend(get_clawlink_tools())
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print(f"[clawlink] load fail: {e}")
|
|
24
|
+
return tools
|
|
25
|
+
|
|
26
|
+
def execute_tool(self, name, args):
|
|
27
|
+
# 기존 툴
|
|
28
|
+
if name=="run_shell":
|
|
29
|
+
return self.sandbox.run_shell(args.get("command",""))
|
|
30
|
+
if name=="read_file":
|
|
31
|
+
return self.sandbox.read_file(args.get("path",""))
|
|
32
|
+
# MCP
|
|
33
|
+
if name.startswith("mcp_"):
|
|
34
|
+
return f"[MCP] {name} called with {args} - 실제 MCP 연결은 npx 실행 필요. pip install mcp 후 확장 예정"
|
|
35
|
+
# Clawlink
|
|
36
|
+
if name.startswith("clawlink_"):
|
|
37
|
+
skill = name.replace("clawlink_","")
|
|
38
|
+
return call_skill(skill, args)
|
|
39
|
+
return f"Unknown tool {name}"
|
|
40
|
+
|
|
41
|
+
# 예전 이름 호환
|
|
42
|
+
def execute(self, name, args):
|
|
43
|
+
return self.execute_tool(name, args)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
TOOLS = [
|
|
2
|
+
{"type":"function","function":{"name":"run_shell","description":"run shell command","parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}},
|
|
3
|
+
{"type":"function","function":{"name":"read_file","description":"read file","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}},
|
|
4
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .factory import get_llm, PROVIDERS
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .base import BaseLLM
|
|
2
|
+
from openai import OpenAI
|
|
3
|
+
import os
|
|
4
|
+
class AlibabaProvider(BaseLLM):
|
|
5
|
+
def __init__(self, api_key, model="qwen-turbo"):
|
|
6
|
+
base=os.getenv("ALIBABA_BASE_URL","https://dashscope-intl.aliyuncs.com/compatible-mode/v1")
|
|
7
|
+
self.client=OpenAI(api_key=api_key, base_url=base); self.model=model
|
|
8
|
+
def chat(self, messages, tools=None):
|
|
9
|
+
kw={"model":self.model,"messages":messages}
|
|
10
|
+
if tools: kw["tools"]=tools
|
|
11
|
+
r=self.client.chat.completions.create(**kw); m=r.choices[0].message
|
|
12
|
+
return {"content":m.content or "","tool_calls":[{"name":tc.function.name,"arguments":tc.function.arguments,"id":tc.id} for tc in (m.tool_calls or [])]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .base import BaseLLM
|
|
2
|
+
from openai import OpenAI
|
|
3
|
+
class DeepSeekProvider(BaseLLM):
|
|
4
|
+
def __init__(self, api_key, model="deepseek-chat"):
|
|
5
|
+
self.client=OpenAI(api_key=api_key, base_url="https://api.deepseek.com"); self.model=model
|
|
6
|
+
def chat(self, messages, tools=None):
|
|
7
|
+
kw={"model":self.model,"messages":messages}
|
|
8
|
+
if tools: kw["tools"]=tools
|
|
9
|
+
r=self.client.chat.completions.create(**kw); m=r.choices[0].message
|
|
10
|
+
return {"content":m.content or "","tool_calls":[{"name":tc.function.name,"arguments":tc.function.arguments,"id":tc.id} for tc in (m.tool_calls or [])]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .openai_provider import OpenAIProvider
|
|
3
|
+
from .gemini_provider import GeminiProvider
|
|
4
|
+
from .deepseek_provider import DeepSeekProvider
|
|
5
|
+
from .llama_provider import LlamaProvider
|
|
6
|
+
from .alibaba_provider import AlibabaProvider
|
|
7
|
+
PROVIDERS={"openai":OpenAIProvider,"gemini":GeminiProvider,"deepseek":DeepSeekProvider,"llama":LlamaProvider,"alibaba":AlibabaProvider}
|
|
8
|
+
def get_llm(provider_name, model=None):
|
|
9
|
+
api_map={"openai":"OPENAI_API_KEY","gemini":"GEMINI_API_KEY","deepseek":"DEEPSEEK_API_KEY","llama":"LLAMA_API_KEY","alibaba":"ALIBABA_API_KEY"}
|
|
10
|
+
key=os.getenv(api_map[provider_name])
|
|
11
|
+
if not key: raise ValueError(f"{api_map[provider_name]} not set")
|
|
12
|
+
return PROVIDERS[provider_name](api_key=key, model=model)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .base import BaseLLM
|
|
2
|
+
import google.generativeai as genai
|
|
3
|
+
class GeminiProvider(BaseLLM):
|
|
4
|
+
def __init__(self, api_key, model="gemini-1.5-flash"):
|
|
5
|
+
genai.configure(api_key=api_key); self.model=genai.GenerativeModel(model)
|
|
6
|
+
def chat(self, messages, tools=None):
|
|
7
|
+
prompt="\n".join([f"{m['role']}: {m['content']}" for m in messages])
|
|
8
|
+
return {"content": self.model.generate_content(prompt).text, "tool_calls":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from openai import OpenAI
|
|
3
|
+
|
|
4
|
+
class LlamaProvider:
|
|
5
|
+
def __init__(self, api_key=None, model=None, **kwargs):
|
|
6
|
+
base = os.getenv("LLAMA_BASE_URL") or "https://ollama.com/v1"
|
|
7
|
+
key = api_key or os.getenv("LLAMA_API_KEY")
|
|
8
|
+
m = model or os.getenv("LLAMA_MODEL") or "gpt-oss:20b"
|
|
9
|
+
if not m or not str(m).strip():
|
|
10
|
+
m = "gpt-oss:20b"
|
|
11
|
+
self.model = str(m).strip()
|
|
12
|
+
print(f"[llama] using model: {self.model} @ {base}")
|
|
13
|
+
self.client = OpenAI(base_url=base, api_key=key)
|
|
14
|
+
|
|
15
|
+
def chat(self, messages, tools=None):
|
|
16
|
+
kw = {"model": self.model, "messages": messages}
|
|
17
|
+
if tools:
|
|
18
|
+
kw["tools"] = tools
|
|
19
|
+
r = self.client.chat.completions.create(**kw)
|
|
20
|
+
msg = r.choices[0].message
|
|
21
|
+
return {
|
|
22
|
+
"content": msg.content or "",
|
|
23
|
+
"tool_calls": [
|
|
24
|
+
{"name": tc.function.name, "arguments": tc.function.arguments, "id": tc.id}
|
|
25
|
+
for tc in (msg.tool_calls or [])
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .base import BaseLLM
|
|
2
|
+
from openai import OpenAI
|
|
3
|
+
class OpenAIProvider(BaseLLM):
|
|
4
|
+
def __init__(self, api_key, model="gpt-4o-mini"):
|
|
5
|
+
self.client=OpenAI(api_key=api_key); self.model=model
|
|
6
|
+
def chat(self, messages, tools=None):
|
|
7
|
+
kw={"model":self.model,"messages":messages}
|
|
8
|
+
if tools: kw["tools"]=tools
|
|
9
|
+
r=self.client.chat.completions.create(**kw)
|
|
10
|
+
m=r.choices[0].message
|
|
11
|
+
return {"content":m.content or "","tool_calls":[{"name":tc.function.name,"arguments":tc.function.arguments,"id":tc.id} for tc in (m.tool_calls or [])]}
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.panel import Panel
|
|
3
|
+
from rich.markdown import Markdown
|
|
4
|
+
from rich.box import ROUNDED
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
RACCOON = """[cyan]
|
|
9
|
+
⢀⣀⣤⣤⣤⣀⡀
|
|
10
|
+
⣠⡾⠋⠁ ⠈⠙⢷
|
|
11
|
+
⣰⠏ 🦝 RACOOGENT ⠹⣆ [bold white]local-first linux agent[/]
|
|
12
|
+
⣠⡾⠋ v0.1.0 ⠈⠙⢷⣄
|
|
13
|
+
⣰⠏ Your Linux, Your AI ⠹⣆
|
|
14
|
+
⠻⣆ ⣰⠟
|
|
15
|
+
⠙⠻⣦⣀⣀⣀⣤⠾⠋
|
|
16
|
+
[/]"""
|
|
17
|
+
|
|
18
|
+
def show_welcome(provider, model, mcp_count, skills):
|
|
19
|
+
console.print(Panel(RACCOON, border_style="cyan", box=ROUNDED, padding=(1,2)))
|
|
20
|
+
info = f"[bold]Provider:[/] {provider} [bold]Model:[/] {model or 'default'} [bold]MCP:[/] {mcp_count}"
|
|
21
|
+
console.print(Panel(info, title="🚀 Ready", border_style="green", box=ROUNDED))
|
|
22
|
+
|
|
23
|
+
def print_agent(text):
|
|
24
|
+
console.print(Panel(Markdown(text), title="🦝 racoogent", border_style="magenta", box=ROUNDED, padding=(1,2)))
|
|
25
|
+
|
|
26
|
+
def print_user(text):
|
|
27
|
+
console.print(Panel(text, title="🧑 You", border_style="blue", box=ROUNDED, padding=(0,1)))
|
|
28
|
+
|
|
29
|
+
def input_box():
|
|
30
|
+
# ANSI 코드 없이 깔끔하게
|
|
31
|
+
console.print() # 한줄 띄우기
|
|
32
|
+
console.print(Panel("", title="💬 You (Enter to send, /bye to exit)", border_style="blue", box=ROUNDED, height=3))
|
|
33
|
+
# 그냥 바로 입력
|
|
34
|
+
try:
|
|
35
|
+
txt = console.input("[bold blue]❯ [/]")
|
|
36
|
+
except EOFError:
|
|
37
|
+
txt = "/bye"
|
|
38
|
+
return txt
|
|
39
|
+
|
|
40
|
+
def print_tool(name, args):
|
|
41
|
+
console.print(f"[dim]⚙ {name} {args}[/]")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
SKILL_FILE = Path.home() / ".config" / "racoogent" / "skills.json"
|
|
4
|
+
SKILL_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
5
|
+
DEFAULTS = {"every-logs": False, "tui": False}
|
|
6
|
+
def load():
|
|
7
|
+
if not SKILL_FILE.exists(): return DEFAULTS.copy()
|
|
8
|
+
try:
|
|
9
|
+
d=json.loads(SKILL_FILE.read_text())
|
|
10
|
+
for k,v in DEFAULTS.items():
|
|
11
|
+
if k not in d: d[k]=v
|
|
12
|
+
return d
|
|
13
|
+
except: return DEFAULTS.copy()
|
|
14
|
+
def save(d): SKILL_FILE.write_text(json.dumps(d,indent=2))
|
|
15
|
+
def is_enabled(n): return load().get(n,False)
|
|
16
|
+
def set_enabled(n,e): d=load(); d[n]=bool(e); save(d)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
CHECKPOINT = Path.home() / ".config" / "racoogent" / "checkpoint.json"
|
|
5
|
+
def save_checkpoint(messages):
|
|
6
|
+
CHECKPOINT.parent.mkdir(parents=True, exist_ok=True)
|
|
7
|
+
data={"timestamp":datetime.now().isoformat(),"messages":messages[-20:]}
|
|
8
|
+
CHECKPOINT.write_text(json.dumps(data,indent=2))
|
|
9
|
+
print(f"💾 Checkpoint saved to {CHECKPOINT}")
|
|
10
|
+
def load_checkpoint():
|
|
11
|
+
if not CHECKPOINT.exists(): return None
|
|
12
|
+
return json.loads(CHECKPOINT.read_text())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: racoogent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Dist: python-dotenv
|
|
7
|
+
Requires-Dist: pyyaml
|
|
8
|
+
Requires-Dist: rich
|
|
9
|
+
Requires-Dist: typer
|
|
10
|
+
Requires-Dist: pydantic
|
|
11
|
+
Requires-Dist: httpx
|
|
12
|
+
Requires-Dist: openai
|
|
13
|
+
Requires-Dist: google-generativeai
|
|
14
|
+
Requires-Dist: litellm
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
racoogent/__init__.py
|
|
5
|
+
racoogent/cli.py
|
|
6
|
+
racoogent/config.py
|
|
7
|
+
racoogent.egg-info/PKG-INFO
|
|
8
|
+
racoogent.egg-info/SOURCES.txt
|
|
9
|
+
racoogent.egg-info/dependency_links.txt
|
|
10
|
+
racoogent.egg-info/entry_points.txt
|
|
11
|
+
racoogent.egg-info/requires.txt
|
|
12
|
+
racoogent.egg-info/top_level.txt
|
|
13
|
+
racoogent/commands/__init__.py
|
|
14
|
+
racoogent/commands/first_setup.py
|
|
15
|
+
racoogent/interconnection/__init__.py
|
|
16
|
+
racoogent/interconnection/clawlink.py
|
|
17
|
+
racoogent/interconnection/mcp_manager.py
|
|
18
|
+
racoogent/interconnection/sandbox.py
|
|
19
|
+
racoogent/interconnection/service.py
|
|
20
|
+
racoogent/interconnection/tools.py
|
|
21
|
+
racoogent/llm/__init__.py
|
|
22
|
+
racoogent/llm/alibaba_provider.py
|
|
23
|
+
racoogent/llm/base.py
|
|
24
|
+
racoogent/llm/deepseek_provider.py
|
|
25
|
+
racoogent/llm/factory.py
|
|
26
|
+
racoogent/llm/gemini_provider.py
|
|
27
|
+
racoogent/llm/llama_provider.py
|
|
28
|
+
racoogent/llm/openai_provider.py
|
|
29
|
+
racoogent/skills/__init__.py
|
|
30
|
+
racoogent/skills/chat_ui.py
|
|
31
|
+
racoogent/skills/manager.py
|
|
32
|
+
racoogent/skills/restoration.py
|
|
33
|
+
racoogent/skills/tui.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
racoogent
|