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.
Files changed (32) hide show
  1. {ollaagent-0.1.2 → ollaagent-0.1.3}/PKG-INFO +1 -1
  2. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/agent.py +4 -4
  3. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/memory.py +2 -2
  4. {ollaagent-0.1.2 → ollaagent-0.1.3}/pyproject.toml +1 -1
  5. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_agent.py +61 -9
  6. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_memory.py +17 -1
  7. {ollaagent-0.1.2 → ollaagent-0.1.3}/uv.lock +1 -1
  8. {ollaagent-0.1.2 → ollaagent-0.1.3}/.agentic_python/guidelines.md +0 -0
  9. {ollaagent-0.1.2 → ollaagent-0.1.3}/.agentic_python/reviewer_role.md +0 -0
  10. {ollaagent-0.1.2 → ollaagent-0.1.3}/.env.sample +0 -0
  11. {ollaagent-0.1.2 → ollaagent-0.1.3}/.gitignore +0 -0
  12. {ollaagent-0.1.2 → ollaagent-0.1.3}/.python-version +0 -0
  13. {ollaagent-0.1.2 → ollaagent-0.1.3}/AGENT.md +0 -0
  14. {ollaagent-0.1.2 → ollaagent-0.1.3}/CLAUDE.md +0 -0
  15. {ollaagent-0.1.2 → ollaagent-0.1.3}/LICENSE +0 -0
  16. {ollaagent-0.1.2 → ollaagent-0.1.3}/README.md +0 -0
  17. {ollaagent-0.1.2 → ollaagent-0.1.3}/README_ko.md +0 -0
  18. {ollaagent-0.1.2 → ollaagent-0.1.3}/main.py +0 -0
  19. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/__init__.py +0 -0
  20. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/config_loader.py +0 -0
  21. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/ollama_client.py +0 -0
  22. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/permissions.py +0 -0
  23. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/plan_mode.py +0 -0
  24. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/subagent.py +0 -0
  25. {ollaagent-0.1.2 → ollaagent-0.1.3}/ollaAgent/tool_bash.py +0 -0
  26. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/__init__.py +0 -0
  27. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/conftest.py +0 -0
  28. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_config_loader.py +0 -0
  29. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_permissions.py +0 -0
  30. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_plan_mode.py +0 -0
  31. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_subagent.py +0 -0
  32. {ollaagent-0.1.2 → ollaagent-0.1.3}/tests/test_tool_bash.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ollaagent
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Local LLM agent powered by ollama — memory, plan mode, subagents
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -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,6 +1,6 @@
1
1
  [project]
2
2
  name = "ollaagent"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Local LLM agent powered by ollama — memory, plan mode, subagents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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
- build_dispatch, execute_tool, list_available_models,
7
- run_agentic_loop, trim_by_tokens, trim_messages)
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="", tool_calls=None, thinking="", done=False, prompt_eval_count=0
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
- msg["content"] = content
177
- if thinking:
178
- msg["thinking"] = thinking
179
- if tool_calls:
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
- from ollaAgent.memory import MemoryEntry, SessionMemory, load_session, save_session
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)
@@ -187,7 +187,7 @@ wheels = [
187
187
 
188
188
  [[package]]
189
189
  name = "ollaagent"
190
- version = "0.1.1"
190
+ version = "0.1.2"
191
191
  source = { editable = "." }
192
192
  dependencies = [
193
193
  { name = "ollama" },
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