ollaagent 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,128 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ import yaml
5
+ from ollaAgent.permissions import DEFAULT_DENY_PATTERNS, PermissionMode
6
+ from pydantic import BaseModel
7
+
8
+ # ──────────────────────────────────────────
9
+ # Config 파일 경로 정의
10
+ # ──────────────────────────────────────────
11
+
12
+ GLOBAL_CONFIG_PATH = Path.home() / ".agents" / "config.yaml"
13
+ PROJECT_CONFIG_PATH = Path.cwd() / "config.yaml"
14
+ LOCAL_CONFIG_PATH = Path.cwd() / ".agents" / "config.yaml"
15
+
16
+ DEFAULT_AGENTS_MD = "AGENT.md"
17
+
18
+
19
+ # ──────────────────────────────────────────
20
+ # AgentConfig Model
21
+ # ──────────────────────────────────────────
22
+
23
+
24
+ class AgentConfig(BaseModel):
25
+ """전체 agent 동작을 제어하는 설정 모델.
26
+
27
+ 계층 우선순위: global < project < local
28
+ 각 레벨에서 기재된 키만 상위 값을 오버라이드한다.
29
+ """
30
+
31
+ model: str = "qwen3-coder-next:latest"
32
+ permission_mode: PermissionMode = PermissionMode.PROMPT
33
+ token_threshold: int = 80_000
34
+ max_iterations: int = 10
35
+ deny_patterns: list[str] = DEFAULT_DENY_PATTERNS
36
+ agents_md_path: str = DEFAULT_AGENTS_MD
37
+
38
+
39
+ # ──────────────────────────────────────────
40
+ # YAML I/O
41
+ # ──────────────────────────────────────────
42
+
43
+
44
+ def _load_yaml(path: Path) -> dict[str, Any]:
45
+ """YAML 파일을 읽어 dict로 반환한다. 파일 없거나 파싱 오류 시 빈 dict 반환."""
46
+ if not path.exists():
47
+ return {}
48
+ try:
49
+ with path.open(encoding="utf-8") as fh:
50
+ data = yaml.safe_load(fh)
51
+ return data if isinstance(data, dict) else {}
52
+ except yaml.YAMLError as exc:
53
+ print(f"[Config] YAML 파싱 오류 ({path}): {exc} — 해당 레벨 skip")
54
+ return {}
55
+
56
+
57
+ def _ensure_global_config(path: Path) -> None:
58
+ """global config 파일이 없으면 기본값으로 자동 생성한다."""
59
+ if path.exists():
60
+ return
61
+ path.parent.mkdir(parents=True, exist_ok=True)
62
+ default = AgentConfig()
63
+ data = {
64
+ "model": default.model,
65
+ "permission_mode": default.permission_mode.value,
66
+ "token_threshold": default.token_threshold,
67
+ "max_iterations": default.max_iterations,
68
+ "deny_patterns": default.deny_patterns,
69
+ "agents_md_path": default.agents_md_path,
70
+ }
71
+ with path.open("w", encoding="utf-8") as fh:
72
+ yaml.dump(data, fh, allow_unicode=True, default_flow_style=False)
73
+ print(f"[Config] global config 생성: {path}")
74
+
75
+
76
+ # ──────────────────────────────────────────
77
+ # 계층 머지
78
+ # ──────────────────────────────────────────
79
+
80
+
81
+ def _merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
82
+ """override에 기재된 키만 base를 교체한다."""
83
+ result = dict(base)
84
+ for key, value in override.items():
85
+ if value is not None:
86
+ result[key] = value
87
+ return result
88
+
89
+
90
+ def load_config(
91
+ global_path: Path = GLOBAL_CONFIG_PATH,
92
+ project_path: Path = PROJECT_CONFIG_PATH,
93
+ local_path: Path = LOCAL_CONFIG_PATH,
94
+ ) -> AgentConfig:
95
+ """global → project → local 순서로 config를 머지하여 AgentConfig를 반환한다."""
96
+ _ensure_global_config(global_path)
97
+
98
+ raw = {}
99
+ for path in (global_path, project_path, local_path):
100
+ raw = _merge(raw, _load_yaml(path))
101
+
102
+ return AgentConfig(
103
+ **{k: v for k, v in raw.items() if k in AgentConfig.model_fields}
104
+ )
105
+
106
+
107
+ # ──────────────────────────────────────────
108
+ # System Prompt Builder
109
+ # ──────────────────────────────────────────
110
+
111
+
112
+ def build_system_prompt(agents_md_path: str) -> str:
113
+ """AGENTS.md 내용을 읽어 system prompt 앞에 prepend한다.
114
+
115
+ 파일이 없으면 기본 system prompt만 반환한다.
116
+ """
117
+ base_prompt = "You are an expert coder. Use tools when needed."
118
+ path = Path(agents_md_path)
119
+ if not path.exists():
120
+ return base_prompt
121
+ content = path.read_text(encoding="utf-8").strip()
122
+ token_estimate = len(content) // 4
123
+ if token_estimate > 5_000:
124
+ print(
125
+ f"[Config] 경고: {agents_md_path} 크기 ~{token_estimate:,} tokens "
126
+ f"— 매 호출마다 context에 포함됨"
127
+ )
128
+ return f"{content}\n\n---\n\n{base_prompt}"
ollaAgent/memory.py ADDED
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ MEMORY_PATH = Path.cwd() / ".agents" / "memory.json"
11
+ SESSION_DIR = Path.cwd() / ".agents" / "sessions"
12
+
13
+
14
+ # ──────────────────────────────────────────
15
+ # Data Model
16
+ # ──────────────────────────────────────────
17
+
18
+
19
+ class MemoryEntry(BaseModel):
20
+ """단일 영구 메모리 항목."""
21
+
22
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
23
+ content: str
24
+ tags: list[str] = Field(default_factory=list)
25
+ created_at: str = Field(
26
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
27
+ )
28
+
29
+
30
+ # ──────────────────────────────────────────
31
+ # Session Memory (JSON 기반)
32
+ # ──────────────────────────────────────────
33
+
34
+
35
+ class SessionMemory:
36
+ """JSON 파일 기반 영구 메모리. /memory 커맨드로 관리한다."""
37
+
38
+ VERSION = "1"
39
+
40
+ def __init__(self, path: Path = MEMORY_PATH) -> None:
41
+ self._path = path
42
+ self._entries: list[MemoryEntry] = []
43
+ self._load()
44
+
45
+ def _load(self) -> None:
46
+ """JSON 파일에서 메모리를 로드한다. 파일 없거나 파싱 오류 시 빈 상태로 시작."""
47
+ if not self._path.exists():
48
+ return
49
+ try:
50
+ with self._path.open(encoding="utf-8") as fh:
51
+ data = json.load(fh)
52
+ self._entries = [MemoryEntry(**e) for e in data.get("entries", [])]
53
+ except (json.JSONDecodeError, KeyError, ValueError):
54
+ self._entries = []
55
+
56
+ def save(self) -> None:
57
+ """현재 메모리를 JSON 파일에 저장한다."""
58
+ self._path.parent.mkdir(parents=True, exist_ok=True)
59
+ data = {
60
+ "version": self.VERSION,
61
+ "entries": [e.model_dump() for e in self._entries],
62
+ }
63
+ with self._path.open("w", encoding="utf-8") as fh:
64
+ json.dump(data, fh, ensure_ascii=False, indent=2)
65
+
66
+ def add(self, content: str, tags: list[str] | None = None) -> MemoryEntry:
67
+ """새 메모리 항목을 추가하고 즉시 저장한다."""
68
+ entry = MemoryEntry(content=content, tags=tags or [])
69
+ self._entries.append(entry)
70
+ self.save()
71
+ return entry
72
+
73
+ def search(self, query: str) -> list[MemoryEntry]:
74
+ """content 또는 tags에서 query를 포함하는 항목을 반환한다."""
75
+ q = query.lower()
76
+ return [
77
+ e
78
+ for e in self._entries
79
+ if q in e.content.lower() or any(q in t.lower() for t in e.tags)
80
+ ]
81
+
82
+ def all(self) -> list[MemoryEntry]:
83
+ """전체 메모리 항목을 반환한다."""
84
+ return list(self._entries)
85
+
86
+ def clear(self) -> int:
87
+ """전체 메모리를 초기화하고 삭제된 개수를 반환한다."""
88
+ count = len(self._entries)
89
+ self._entries = []
90
+ self.save()
91
+ return count
92
+
93
+ def to_context_string(self) -> str:
94
+ """메모리 항목을 system prompt에 주입할 텍스트로 변환한다."""
95
+ if not self._entries:
96
+ return ""
97
+ lines = ["## Persistent Memory"]
98
+ for i, e in enumerate(self._entries, 1):
99
+ tag_str = f" [{', '.join(e.tags)}]" if e.tags else ""
100
+ lines.append(f"{i}. {e.content}{tag_str}")
101
+ return "\n".join(lines)
102
+
103
+
104
+ # ──────────────────────────────────────────
105
+ # Session Save / Load
106
+ # ──────────────────────────────────────────
107
+
108
+
109
+ def save_session(messages: list[dict], path: Path) -> None:
110
+ """대화 히스토리를 JSON 파일로 저장한다."""
111
+ path.parent.mkdir(parents=True, exist_ok=True)
112
+ data = {
113
+ "version": "1",
114
+ "saved_at": datetime.now(timezone.utc).isoformat(),
115
+ "messages": messages,
116
+ }
117
+ with path.open("w", encoding="utf-8") as fh:
118
+ json.dump(data, fh, ensure_ascii=False, indent=2)
119
+
120
+
121
+ def load_session(path: Path) -> list[dict]:
122
+ """저장된 세션 JSON에서 messages를 로드한다. 실패 시 빈 리스트 반환."""
123
+ if not path.exists():
124
+ return []
125
+ try:
126
+ with path.open(encoding="utf-8") as fh:
127
+ data = json.load(fh)
128
+ return data.get("messages", [])
129
+ except (json.JSONDecodeError, KeyError):
130
+ return []
@@ -0,0 +1,26 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from ollama import Client
4
+
5
+ load_dotenv()
6
+
7
+ client = Client(
8
+ host='https://ollama.nabee.ai.kr',
9
+ headers={
10
+ 'CF-Access-Client-Id': os.getenv('CF_ACCESS_CLIENT_ID'),
11
+ 'CF-Access-Client-Secret': os.getenv('CF_ACCESS_CLIENT_SECRET'),
12
+ }
13
+ )
14
+
15
+ response = client.chat(
16
+ model='qwen3-coder-next:latest',
17
+ messages=[{'role': 'user', 'content': 'Hello'}]
18
+ )
19
+
20
+ thinking = response['message'].get('thinking')
21
+ if thinking:
22
+ print("=== Thinking ===")
23
+ print(thinking)
24
+ print("=== Response ===")
25
+
26
+ print(response['message']['content'])
@@ -0,0 +1,49 @@
1
+ import re
2
+ from enum import Enum
3
+
4
+ from pydantic import BaseModel
5
+ from rich.prompt import Confirm
6
+
7
+
8
+ class PermissionMode(str, Enum):
9
+ """bash tool 실행 허가 모드."""
10
+
11
+ AUTO = "auto" # deny list 외 모두 자동 허가
12
+ PROMPT = "prompt" # deny list 외 사용자에게 확인
13
+ DENY = "deny" # 모든 명령 거부
14
+
15
+
16
+ DEFAULT_DENY_PATTERNS: list[str] = [
17
+ r"rm\s+.*-rf|rm\s+-rf", # 재귀 삭제
18
+ r"sudo\s+", # 권한 상승
19
+ r"dd\s+if=", # 디스크 덮어쓰기
20
+ r"mkfs", # 파일시스템 포맷
21
+ r":\(\)\{.*\}", # fork bomb
22
+ r">\s*/dev/", # 디바이스 직접 쓰기
23
+ r"curl.+\|.+sh|wget.+\|.+sh", # 원격 스크립트 실행
24
+ r"chmod\s+777", # 위험한 권한 변경
25
+ ]
26
+
27
+
28
+ class PermissionConfig(BaseModel):
29
+ """bash tool 실행 허가 설정."""
30
+
31
+ mode: PermissionMode = PermissionMode.PROMPT
32
+ deny_patterns: list[str] = DEFAULT_DENY_PATTERNS
33
+
34
+
35
+ def is_denied(command: str, config: PermissionConfig) -> tuple[bool, str]:
36
+ """deny_patterns 중 하나라도 매칭되면 (True, 매칭된 패턴)을 반환한다."""
37
+ for pattern in config.deny_patterns:
38
+ if re.search(pattern, command, re.IGNORECASE):
39
+ return True, pattern
40
+ return False, ""
41
+
42
+
43
+ def request_permission(command: str, config: PermissionConfig) -> bool:
44
+ """mode에 따라 실행 허가 여부를 반환한다. deny 체크는 포함하지 않는다."""
45
+ if config.mode == PermissionMode.DENY:
46
+ return False
47
+ if config.mode == PermissionMode.AUTO:
48
+ return True
49
+ return Confirm.ask(f"[yellow]Allow command?[/yellow]\n {command}")
ollaAgent/plan_mode.py ADDED
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ollama import Client
6
+ from rich.console import Console
7
+ from rich.live import Live
8
+ from rich.markdown import Markdown
9
+ from rich.panel import Panel
10
+
11
+ console = Console()
12
+
13
+ PLAN_SYSTEM_PREFIX = (
14
+ "You are in PLAN MODE. Do NOT call any tools. "
15
+ "Only plan, no execution. "
16
+ "Return a structured step-by-step plan."
17
+ )
18
+
19
+
20
+ # ──────────────────────────────────────────
21
+ # Public API
22
+ # ──────────────────────────────────────────
23
+
24
+
25
+ def run_plan(
26
+ task: str,
27
+ client: Client,
28
+ model: str,
29
+ base_prompt: str = "",
30
+ ) -> str:
31
+ """plan 모드: tools=[] 로 모델을 호출해 실행 없이 계획만 반환한다.
32
+
33
+ Args:
34
+ task: 계획할 작업 설명.
35
+ client: Ollama 클라이언트.
36
+ model: 사용할 모델 이름.
37
+ base_prompt: 추가 컨텍스트 (optional).
38
+
39
+ Returns:
40
+ 모델이 생성한 계획 텍스트.
41
+ """
42
+ sys_content = (
43
+ f"{PLAN_SYSTEM_PREFIX}\n\n{base_prompt}" if base_prompt else PLAN_SYSTEM_PREFIX
44
+ )
45
+ messages: list[dict[str, str]] = [
46
+ {"role": "system", "content": sys_content},
47
+ {"role": "user", "content": task},
48
+ ]
49
+ console.print(Panel("[bold blue]PLAN MODE[/] — tools disabled", style="blue"))
50
+ stream = client.chat(
51
+ model=model,
52
+ messages=messages,
53
+ tools=[],
54
+ stream=True,
55
+ )
56
+ return _stream_plan(stream)
57
+
58
+
59
+ # ──────────────────────────────────────────
60
+ # Private Helpers
61
+ # ──────────────────────────────────────────
62
+
63
+
64
+ def _stream_plan(stream: Any) -> str:
65
+ """스트림을 소비하며 content를 누적하고 Live 렌더링 후 반환한다."""
66
+ content = ""
67
+ with Live(console=console, refresh_per_second=10) as live:
68
+ for chunk in stream:
69
+ piece = chunk.get("message", {}).get("content", "")
70
+ if piece:
71
+ content += piece
72
+ live.update(Markdown(content))
73
+ return content
ollaAgent/subagent.py ADDED
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from multiprocessing import Pool
5
+ from typing import Any
6
+
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+
11
+
12
+ # ──────────────────────────────────────────
13
+ # Data Model
14
+ # ──────────────────────────────────────────
15
+
16
+
17
+ @dataclass
18
+ class SubagentTask:
19
+ """단일 서브에이전트 실행 단위. multiprocessing pickle을 위해 기본 타입만 사용한다."""
20
+
21
+ name: str
22
+ task: str
23
+ model: str
24
+ host: str
25
+ cf_client_id: str
26
+ cf_client_secret: str
27
+ system_prompt: str = "You are an expert coder. Answer concisely."
28
+ max_iterations: int = 5
29
+
30
+
31
+ # ──────────────────────────────────────────
32
+ # Worker (모듈 레벨 — spawn 방식 pickle 조건)
33
+ # ──────────────────────────────────────────
34
+
35
+
36
+ def _simple_loop(
37
+ messages: list[dict],
38
+ client: Any,
39
+ model: str,
40
+ max_iterations: int,
41
+ ) -> str:
42
+ """Rich 없이 스트리밍으로 단일 응답을 수집해 반환한다.
43
+
44
+ tools=[] 이므로 tool_call 없이 한 번에 완료된다.
45
+ """
46
+ final = ""
47
+ for _ in range(max_iterations):
48
+ stream = client.chat(
49
+ model=model,
50
+ messages=messages,
51
+ tools=[],
52
+ stream=True,
53
+ )
54
+ content = ""
55
+ for chunk in stream:
56
+ content += chunk.get("message", {}).get("content", "")
57
+ messages.append({"role": "assistant", "content": content})
58
+ final = content
59
+ break # tools=[] 이므로 첫 응답에서 완료
60
+ return final
61
+
62
+
63
+ def _worker(task: SubagentTask) -> tuple[str, str]:
64
+ """워커 프로세스: 독립 Client로 태스크를 실행하고 (name, result) 튜플을 반환한다.
65
+
66
+ Rich Live는 사용하지 않는다 — 멀티프로세싱 환경에서 터미널 출력이 충돌한다.
67
+ Client import를 함수 내부에 위치시켜 worker 프로세스에서 fresh import가 이루어지도록 한다.
68
+ """
69
+ from ollama import Client # noqa: PLC0415 — subprocess fresh import 의도적
70
+
71
+ client = Client(
72
+ host=task.host,
73
+ headers={
74
+ "CF-Access-Client-Id": task.cf_client_id,
75
+ "CF-Access-Client-Secret": task.cf_client_secret,
76
+ },
77
+ )
78
+ messages: list[dict[str, str]] = [
79
+ {"role": "system", "content": task.system_prompt},
80
+ {"role": "user", "content": task.task},
81
+ ]
82
+ result = _simple_loop(messages, client, task.model, task.max_iterations)
83
+ return (task.name, result)
84
+
85
+
86
+ # ──────────────────────────────────────────
87
+ # Public API
88
+ # ──────────────────────────────────────────
89
+
90
+
91
+ def run_subagents(
92
+ tasks: list[SubagentTask],
93
+ workers: int = 4,
94
+ ) -> list[tuple[str, str]]:
95
+ """멀티프로세싱으로 SubagentTask 목록을 병렬 실행하고 (name, result) 목록을 반환한다.
96
+
97
+ Args:
98
+ tasks: 실행할 SubagentTask 목록.
99
+ workers: 최대 워커 프로세스 수 (기본 4, tasks 수가 더 적으면 tasks 수 사용).
100
+
101
+ Returns:
102
+ [(name, result), ...] 형태의 결과 목록.
103
+ """
104
+ if not tasks:
105
+ return []
106
+ num_workers = min(workers, len(tasks))
107
+ with Pool(processes=num_workers) as pool:
108
+ return pool.map(_worker, tasks)
ollaAgent/tool_bash.py ADDED
@@ -0,0 +1,38 @@
1
+ import subprocess
2
+ from typing import Any
3
+
4
+ from ollaAgent.permissions import PermissionConfig, is_denied, request_permission
5
+
6
+
7
+ def tool_bash(args: dict[str, Any], config: PermissionConfig) -> str:
8
+ """bash 명령을 실행하고 stdout/stderr를 반환한다.
9
+
10
+ 실행 순서:
11
+ 1. deny_patterns 매칭 여부 확인 → 매칭 시 즉시 차단
12
+ 2. mode에 따라 사용자 허가 요청 (prompt) 또는 자동 허가 (auto)
13
+ 3. subprocess로 실행 (shell=True 금지, timeout=30)
14
+ """
15
+ command = args.get("command", "")
16
+ if not command:
17
+ return "ERROR: No command provided"
18
+
19
+ denied, pattern = is_denied(command, config)
20
+ if denied:
21
+ return f"ERROR: Blocked - command matches deny pattern: '{pattern}'"
22
+
23
+ if not request_permission(command, config):
24
+ return "ERROR: Permission denied by user"
25
+
26
+ try:
27
+ result = subprocess.run(
28
+ ["bash", "-c", command],
29
+ capture_output=True,
30
+ text=True,
31
+ timeout=30,
32
+ )
33
+ output = result.stdout or result.stderr
34
+ return output.strip() if output.strip() else "(no output)"
35
+ except subprocess.TimeoutExpired:
36
+ return "ERROR: Timeout (30s)"
37
+ except Exception as exc:
38
+ return f"ERROR: {exc}"
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: ollaagent
3
+ Version: 0.1.0
4
+ Summary: Local LLM agent powered by ollama — memory, plan mode, subagents
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: ollama==0.6.1
8
+ Requires-Dist: pydantic==2.12.5
9
+ Requires-Dist: python-dotenv==1.2.2
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: rich==14.3.3
12
+ Description-Content-Type: text/markdown
13
+
14
+ # ollaAgent
15
+
16
+ A local LLM agent powered by [ollama](https://ollama.com) — with persistent memory, plan mode, and parallel subagents.
17
+
18
+ [![Python](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org)
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
20
+
21
+ ## Features
22
+
23
+ | Feature | Description |
24
+ |---------|-------------|
25
+ | **Agent Loop** | Iterative tool-calling loop with `run_python`, `run_bash`, `read_file`, `write_file`, `list_files` |
26
+ | **Persistent Memory** | JSON-backed memory with `/memory add/list/search/clear` commands |
27
+ | **Session Saving** | Conversation history auto-saved to `.agents/sessions/` on exit |
28
+ | **Plan Mode** | `/plan <task>` — generates a structured step-by-step plan without executing tools |
29
+ | **Subagents** | `/subagent` — runs multiple ollama instances in parallel via `multiprocessing.Pool` |
30
+ | **Permission Control** | Configurable allow/deny patterns for bash commands |
31
+ | **Cloudflare Access** | Supports CF-Access headers for tunneled ollama endpoints |
32
+
33
+ ## Requirements
34
+
35
+ - Python 3.11+
36
+ - [ollama](https://ollama.com) running locally (or via Cloudflare Access)
37
+ - [uv](https://docs.astral.sh/uv/)
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ git clone https://github.com/github010000/ollaAgent
43
+ cd ollaAgent
44
+ uv sync
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```bash
50
+ # Start the agent
51
+ uv run ollaagent
52
+
53
+ # With a specific model
54
+ uv run ollaagent --model qwen2.5-coder:7b
55
+
56
+ # With a remote ollama host
57
+ uv run ollaagent --host https://your-ollama.example.com
58
+ ```
59
+
60
+ ## Built-in Commands
61
+
62
+ | Command | Description |
63
+ |---------|-------------|
64
+ | `/plan <task>` | Generate a step-by-step plan (no execution) |
65
+ | `/subagent` | Run tasks in parallel across multiple ollama instances |
66
+ | `/memory add <text>` | Add an entry to persistent memory |
67
+ | `/memory list` | List all memory entries |
68
+ | `/memory search <query>` | Search memory by keyword |
69
+ | `/memory clear` | Clear all memory entries |
70
+ | `/exit` | Exit the agent |
71
+
72
+ ## Subagent Usage
73
+
74
+ Single model across all tasks:
75
+ ```
76
+ /subagent
77
+ > --model llama3:8b task one | task two | task three
78
+ ```
79
+
80
+ Per-task model assignment:
81
+ ```
82
+ > @qwen2.5-coder:7b write a sorting algorithm | @llama3:8b explain it
83
+ ```
84
+
85
+ ## Configuration
86
+
87
+ Create a `.env` file in the project root:
88
+
89
+ ```env
90
+ OLLAMA_HOST=http://localhost:11434
91
+ CF_CLIENT_ID=
92
+ CF_CLIENT_SECRET=
93
+ ```
94
+
95
+ ## Project Structure
96
+
97
+ ```
98
+ ollaAgent/
99
+ ├── agent.py # Main agent loop & CLI entry point
100
+ ├── memory.py # Persistent memory (JSON)
101
+ ├── plan_mode.py # Plan-only mode (tools=[])
102
+ ├── subagent.py # Parallel subagents via multiprocessing
103
+ ├── tool_bash.py # Bash tool with permission control
104
+ ├── permissions.py # Allow/deny pattern matching
105
+ ├── config_loader.py # YAML config & system prompt builder
106
+ └── ollama_client.py # Ollama client factory
107
+ ```
108
+
109
+ ## Running Tests
110
+
111
+ ```bash
112
+ uv run pytest
113
+ ```
114
+
115
+ ## License
116
+
117
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,14 @@
1
+ ollaAgent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ollaAgent/agent.py,sha256=JXVFdOJcR19CHLM0dh2zKLUWYOXFdxWWIytoFUh_SkA,24643
3
+ ollaAgent/config_loader.py,sha256=v-Q712wqaLsZ3ssYhgt3T7WgFNgy8eh__H9-G0qu0tI,4970
4
+ ollaAgent/memory.py,sha256=qzRxl0YaKHUBIA3WRZCpJU0_GWr1tw4SO97SHUhcoMA,4762
5
+ ollaAgent/ollama_client.py,sha256=J8wRHKKs0FpEyAvJeOJe0XGadRihLU1xbC0dHKGRWyE,595
6
+ ollaAgent/permissions.py,sha256=_D4H2Xn1xdhIF1NDvMvWxn6dnx7bHFPDDoMlpXm7_iA,1607
7
+ ollaAgent/plan_mode.py,sha256=9sGGe_fzueJ_tgLLdYttqQFGivyfIlgG8fPJWgCxdo0,2300
8
+ ollaAgent/subagent.py,sha256=TDRfa42NCzYFRCNetOvmI7m1PS0l0keTUG8A1Xz8xXw,3713
9
+ ollaAgent/tool_bash.py,sha256=5cqcB1U8YBfsoIOyAdgw2H9HEajB1rxDETYkNr3_AOY,1268
10
+ ollaagent-0.1.0.dist-info/METADATA,sha256=qcBHudiAwmnR1NnfcCyZwXRXaBx6cwLpSaLJ9g6BeV8,3315
11
+ ollaagent-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
+ ollaagent-0.1.0.dist-info/entry_points.txt,sha256=c7ENjzdJDXPpSvVHZTdM2M_cNZEPLSX3cm5z9AUmUMA,51
13
+ ollaagent-0.1.0.dist-info/licenses/LICENSE,sha256=ePCDpHnSmBcdbm9YJSHND3Gir6wnkyOIcpo6tsg1m9A,1069
14
+ ollaagent-0.1.0.dist-info/RECORD,,