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.
Files changed (35) hide show
  1. racoogent-0.1.0/LICENSE +21 -0
  2. racoogent-0.1.0/PKG-INFO +15 -0
  3. racoogent-0.1.0/README.md +34 -0
  4. racoogent-0.1.0/pyproject.toml +7 -0
  5. racoogent-0.1.0/racoogent/__init__.py +1 -0
  6. racoogent-0.1.0/racoogent/cli.py +164 -0
  7. racoogent-0.1.0/racoogent/commands/__init__.py +0 -0
  8. racoogent-0.1.0/racoogent/commands/first_setup.py +81 -0
  9. racoogent-0.1.0/racoogent/config.py +17 -0
  10. racoogent-0.1.0/racoogent/interconnection/__init__.py +1 -0
  11. racoogent-0.1.0/racoogent/interconnection/clawlink.py +48 -0
  12. racoogent-0.1.0/racoogent/interconnection/mcp_manager.py +42 -0
  13. racoogent-0.1.0/racoogent/interconnection/sandbox.py +15 -0
  14. racoogent-0.1.0/racoogent/interconnection/service.py +43 -0
  15. racoogent-0.1.0/racoogent/interconnection/tools.py +4 -0
  16. racoogent-0.1.0/racoogent/llm/__init__.py +1 -0
  17. racoogent-0.1.0/racoogent/llm/alibaba_provider.py +12 -0
  18. racoogent-0.1.0/racoogent/llm/base.py +4 -0
  19. racoogent-0.1.0/racoogent/llm/deepseek_provider.py +10 -0
  20. racoogent-0.1.0/racoogent/llm/factory.py +12 -0
  21. racoogent-0.1.0/racoogent/llm/gemini_provider.py +8 -0
  22. racoogent-0.1.0/racoogent/llm/llama_provider.py +27 -0
  23. racoogent-0.1.0/racoogent/llm/openai_provider.py +11 -0
  24. racoogent-0.1.0/racoogent/skills/__init__.py +0 -0
  25. racoogent-0.1.0/racoogent/skills/chat_ui.py +41 -0
  26. racoogent-0.1.0/racoogent/skills/manager.py +16 -0
  27. racoogent-0.1.0/racoogent/skills/restoration.py +12 -0
  28. racoogent-0.1.0/racoogent/skills/tui.py +10 -0
  29. racoogent-0.1.0/racoogent.egg-info/PKG-INFO +15 -0
  30. racoogent-0.1.0/racoogent.egg-info/SOURCES.txt +33 -0
  31. racoogent-0.1.0/racoogent.egg-info/dependency_links.txt +1 -0
  32. racoogent-0.1.0/racoogent.egg-info/entry_points.txt +2 -0
  33. racoogent-0.1.0/racoogent.egg-info/requires.txt +9 -0
  34. racoogent-0.1.0/racoogent.egg-info/top_level.txt +1 -0
  35. racoogent-0.1.0/setup.cfg +4 -0
@@ -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.
@@ -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,7 @@
1
+ [project]
2
+ name="racoogent"
3
+ version="0.1.0"
4
+ requires-python=">=3.10"
5
+ dependencies=["python-dotenv","pyyaml","rich","typer","pydantic","httpx","openai","google-generativeai","litellm"]
6
+ [project.scripts]
7
+ racoogent="racoogent.cli:app"
@@ -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,4 @@
1
+ from abc import ABC, abstractmethod
2
+ class BaseLLM(ABC):
3
+ @abstractmethod
4
+ def chat(self, messages, tools=None): pass
@@ -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,10 @@
1
+ RACCOON_LOGO = r"""
2
+ ⠀⢀⣀⣤⣤⣤⣀⡀
3
+ ⣠⡾⠋⠁⠀⠀⠈⠙⢷
4
+ ⣰⠏ 🦝 RACOOGENT ⠹⣆
5
+ ⠀⣠⡾⠋⠁ v0.1.0 ⠈⠙⢷⣄
6
+ ⣰⠏ local-first agent ⠹⣆
7
+ ⠻⣆ Your Linux, Your AI ⣰⠟
8
+ ⠙⠻⣦⣀⣀⣀⣤⠾⠋
9
+ """
10
+ def show(): print(f"\033[96m{RACCOON_LOGO}\033[0m")
@@ -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,2 @@
1
+ [console_scripts]
2
+ racoogent = racoogent.cli:app
@@ -0,0 +1,9 @@
1
+ python-dotenv
2
+ pyyaml
3
+ rich
4
+ typer
5
+ pydantic
6
+ httpx
7
+ openai
8
+ google-generativeai
9
+ litellm
@@ -0,0 +1 @@
1
+ racoogent
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+