ollaagent 0.1.2__tar.gz → 0.1.3__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.
- {ollaagent-0.1.2 → ollaagent-0.1.3}/PKG-INFO +1 -1
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/agent.py +4 -4
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/memory.py +2 -2
- {ollaagent-0.1.2 → ollaagent-0.1.3}/pyproject.toml +1 -1
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_agent.py +61 -9
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_memory.py +17 -1
- {ollaagent-0.1.2 → ollaagent-0.1.3}/uv.lock +1 -1
- {ollaagent-0.1.2 → ollaagent-0.1.3}/.agentic_python/guidelines.md +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/.agentic_python/reviewer_role.md +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/.env.sample +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/.gitignore +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/.python-version +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/AGENT.md +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/CLAUDE.md +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/LICENSE +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/README.md +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/README_ko.md +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/main.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/__init__.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/config_loader.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/ollama_client.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/permissions.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/plan_mode.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/subagent.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/tool_bash.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/__init__.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/conftest.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_config_loader.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_permissions.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_plan_mode.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_subagent.py +0 -0
- {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_tool_bash.py +0 -0
|
@@ -358,7 +358,7 @@ def _accumulate_tool_calls(msg: dict[str, Any], accumulated: dict[int, dict]) ->
|
|
|
358
358
|
idx = tc.get("index", len(accumulated))
|
|
359
359
|
if idx not in accumulated:
|
|
360
360
|
accumulated[idx] = {"name": "", "arguments": ""}
|
|
361
|
-
fn = tc.get("function"
|
|
361
|
+
fn = tc.get("function") or {}
|
|
362
362
|
accumulated[idx]["name"] += fn.get("name", "")
|
|
363
363
|
raw_args = fn.get("arguments", "")
|
|
364
364
|
if isinstance(raw_args, dict):
|
|
@@ -403,11 +403,11 @@ def _stream_response(stream: Any) -> tuple[str, str, dict[int, dict], int]:
|
|
|
403
403
|
|
|
404
404
|
with Live(console=console, refresh_per_second=10) as live:
|
|
405
405
|
for chunk in stream:
|
|
406
|
-
msg = chunk.get("message"
|
|
407
|
-
thinking = msg.get("thinking"
|
|
406
|
+
msg = chunk.get("message") or {}
|
|
407
|
+
thinking = msg.get("thinking") or ""
|
|
408
408
|
if thinking:
|
|
409
409
|
thinking_content += thinking
|
|
410
|
-
content = msg.get("content"
|
|
410
|
+
content = msg.get("content") or ""
|
|
411
411
|
if content:
|
|
412
412
|
assistant_content += content
|
|
413
413
|
live.update(Markdown(assistant_content))
|
|
@@ -49,7 +49,7 @@ class SessionMemory:
|
|
|
49
49
|
try:
|
|
50
50
|
with self._path.open(encoding="utf-8") as fh:
|
|
51
51
|
data = json.load(fh)
|
|
52
|
-
self._entries = [MemoryEntry(**e) for e in data.get("entries"
|
|
52
|
+
self._entries = [MemoryEntry(**e) for e in (data.get("entries") or [])]
|
|
53
53
|
except (json.JSONDecodeError, KeyError, ValueError):
|
|
54
54
|
self._entries = []
|
|
55
55
|
|
|
@@ -125,6 +125,6 @@ def load_session(path: Path) -> list[dict]:
|
|
|
125
125
|
try:
|
|
126
126
|
with path.open(encoding="utf-8") as fh:
|
|
127
127
|
data = json.load(fh)
|
|
128
|
-
return data.get("messages"
|
|
128
|
+
return data.get("messages") or []
|
|
129
129
|
except (json.JSONDecodeError, KeyError):
|
|
130
130
|
return []
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from unittest.mock import MagicMock, patch
|
|
2
2
|
|
|
3
|
-
import ollaAgent.agent as agent
|
|
4
3
|
import pytest
|
|
4
|
+
|
|
5
|
+
import ollaAgent.agent as agent
|
|
5
6
|
from ollaAgent.agent import (_is_model_available, _parse_subagent_input,
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
build_dispatch, execute_tool,
|
|
8
|
+
list_available_models, run_agentic_loop,
|
|
9
|
+
trim_by_tokens, trim_messages)
|
|
8
10
|
from ollaAgent.permissions import PermissionConfig, PermissionMode
|
|
9
11
|
|
|
10
12
|
_TEST_DISPATCH = build_dispatch(PermissionConfig(mode=PermissionMode.AUTO))
|
|
@@ -169,14 +171,28 @@ class TestTrimMessages:
|
|
|
169
171
|
|
|
170
172
|
|
|
171
173
|
def _make_chunk(
|
|
172
|
-
content="",
|
|
174
|
+
content="",
|
|
175
|
+
tool_calls=None,
|
|
176
|
+
thinking="",
|
|
177
|
+
done=False,
|
|
178
|
+
prompt_eval_count=0,
|
|
179
|
+
null_tool_calls=False,
|
|
180
|
+
null_message=False,
|
|
181
|
+
null_content=False,
|
|
182
|
+
null_thinking=False,
|
|
173
183
|
):
|
|
184
|
+
"""테스트용 stream chunk 생성.
|
|
185
|
+
|
|
186
|
+
null_* 플래그: 키는 존재하지만 값이 null인 실제 API 응답 재현용.
|
|
187
|
+
"""
|
|
188
|
+
if null_message:
|
|
189
|
+
return {"message": None}
|
|
174
190
|
msg = {}
|
|
175
|
-
if content
|
|
176
|
-
|
|
177
|
-
if
|
|
178
|
-
msg["
|
|
179
|
-
|
|
191
|
+
msg["content"] = None if null_content else (content or None)
|
|
192
|
+
msg["thinking"] = None if null_thinking else (thinking or None)
|
|
193
|
+
if null_tool_calls:
|
|
194
|
+
msg["tool_calls"] = None # 실제 API: "tool_calls": null
|
|
195
|
+
elif tool_calls:
|
|
180
196
|
msg["tool_calls"] = tool_calls
|
|
181
197
|
chunk = {"message": msg}
|
|
182
198
|
if done:
|
|
@@ -295,6 +311,42 @@ class TestAgenticLoop:
|
|
|
295
311
|
assert "최종 답변입니다." in result
|
|
296
312
|
assert "내부적으로 생각 중..." not in result
|
|
297
313
|
|
|
314
|
+
def test_null_tool_calls_in_stream_does_not_raise(self):
|
|
315
|
+
"""실제 API가 tool_calls: null 반환 시 TypeError 없이 정상 처리"""
|
|
316
|
+
mock_client = MagicMock()
|
|
317
|
+
mock_client.chat.return_value = _make_stream(
|
|
318
|
+
_make_chunk(null_tool_calls=True),
|
|
319
|
+
_make_chunk(content="응답입니다."),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
messages = [{"role": "user", "content": "안녕"}]
|
|
323
|
+
result = run_agentic_loop(messages, mock_client, _TEST_DISPATCH)
|
|
324
|
+
assert "응답입니다." in result
|
|
325
|
+
|
|
326
|
+
def test_null_message_in_stream_does_not_raise(self):
|
|
327
|
+
"""실제 API가 message: null 반환 시 정상 처리"""
|
|
328
|
+
mock_client = MagicMock()
|
|
329
|
+
mock_client.chat.return_value = _make_stream(
|
|
330
|
+
_make_chunk(null_message=True),
|
|
331
|
+
_make_chunk(content="응답입니다."),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
messages = [{"role": "user", "content": "안녕"}]
|
|
335
|
+
result = run_agentic_loop(messages, mock_client, _TEST_DISPATCH)
|
|
336
|
+
assert "응답입니다." in result
|
|
337
|
+
|
|
338
|
+
def test_null_content_and_thinking_in_stream_does_not_raise(self):
|
|
339
|
+
"""content, thinking이 null인 chunk 정상 처리"""
|
|
340
|
+
mock_client = MagicMock()
|
|
341
|
+
mock_client.chat.return_value = _make_stream(
|
|
342
|
+
_make_chunk(null_content=True, null_thinking=True),
|
|
343
|
+
_make_chunk(content="정상 응답."),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
messages = [{"role": "user", "content": "테스트"}]
|
|
347
|
+
result = run_agentic_loop(messages, mock_client, _TEST_DISPATCH)
|
|
348
|
+
assert "정상 응답." in result
|
|
349
|
+
|
|
298
350
|
|
|
299
351
|
# ──────────────────────────────────────────
|
|
300
352
|
# Unit Tests: write_file
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
from ollaAgent.memory import (MemoryEntry, SessionMemory, load_session,
|
|
6
|
+
save_session)
|
|
5
7
|
|
|
6
8
|
# ──────────────────────────────────────────
|
|
7
9
|
# Unit Tests: MemoryEntry
|
|
@@ -119,6 +121,13 @@ class TestSessionMemoryPersistence:
|
|
|
119
121
|
mem = SessionMemory(path=path)
|
|
120
122
|
assert mem.all() == []
|
|
121
123
|
|
|
124
|
+
def test_null_entries_key_gives_empty(self, tmp_path):
|
|
125
|
+
"""JSON에 entries: null 포함 시 빈 리스트로 처리"""
|
|
126
|
+
path = tmp_path / "null_entries.json"
|
|
127
|
+
path.write_text(json.dumps({"entries": None}))
|
|
128
|
+
mem = SessionMemory(path=path)
|
|
129
|
+
assert mem.all() == []
|
|
130
|
+
|
|
122
131
|
|
|
123
132
|
class TestSessionMemoryContextString:
|
|
124
133
|
|
|
@@ -174,6 +183,13 @@ class TestSaveLoadSession:
|
|
|
174
183
|
result = load_session(path)
|
|
175
184
|
assert result == []
|
|
176
185
|
|
|
186
|
+
def test_load_null_messages_key_returns_empty(self, tmp_path):
|
|
187
|
+
"""JSON에 messages: null 포함 시 빈 리스트 반환"""
|
|
188
|
+
path = tmp_path / "null_messages.json"
|
|
189
|
+
path.write_text(json.dumps({"messages": None}))
|
|
190
|
+
result = load_session(path)
|
|
191
|
+
assert result == []
|
|
192
|
+
|
|
177
193
|
def test_save_creates_parent_dirs(self, tmp_path):
|
|
178
194
|
path = tmp_path / "a" / "b" / "c" / "sess.json"
|
|
179
195
|
save_session([{"role": "user", "content": "x"}], path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|