agentnova 0.3.8__tar.gz → 0.3.9__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 (92) hide show
  1. {agentnova-0.3.8 → agentnova-0.3.9}/PKG-INFO +5 -7
  2. {agentnova-0.3.8 → agentnova-0.3.9}/README.md +4 -6
  3. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/__init__.py +1 -1
  4. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/agent.py +31 -18
  5. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/backends/ollama.py +31 -4
  6. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/__init__.py +7 -7
  7. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/helpers.py +38 -3
  8. agentnova-0.3.9/agentnova/core/model_config.py +36 -0
  9. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/model_family_config.py +51 -2
  10. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/models.py +6 -1
  11. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/prompts.py +14 -1
  12. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/tool_parse.py +46 -43
  13. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/tools/builtins.py +1 -9
  14. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/tools/registry.py +1 -1
  15. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova.egg-info/PKG-INFO +5 -7
  16. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova.egg-info/SOURCES.txt +2 -0
  17. {agentnova-0.3.8 → agentnova-0.3.9}/pyproject.toml +1 -1
  18. agentnova-0.3.9/tests/test_builtins.py +349 -0
  19. agentnova-0.3.9/tests/test_security.py +406 -0
  20. agentnova-0.3.8/agentnova/core/model_config.py +0 -239
  21. {agentnova-0.3.8 → agentnova-0.3.9}/LICENSE +0 -0
  22. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/__main__.py +0 -0
  23. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/acp_plugin.py +0 -0
  24. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/agent_mode.py +0 -0
  25. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/backends/__init__.py +0 -0
  26. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/backends/base.py +0 -0
  27. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/backends/bitnet.py +0 -0
  28. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/cli.py +0 -0
  29. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/colors.py +0 -0
  30. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/config.py +0 -0
  31. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/args_normal.py +0 -0
  32. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/error_recovery.py +0 -0
  33. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/math_prompts.py +0 -0
  34. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/memory.py +0 -0
  35. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/openresponses.py +0 -0
  36. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/tool_cache.py +0 -0
  37. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/core/types.py +0 -0
  38. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/00_basic_agent.py +0 -0
  39. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/01_quick_diagnostic.py +0 -0
  40. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/02_tool_test.py +0 -0
  41. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/03_reasoning_test.py +0 -0
  42. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/04_gsm8k_benchmark.py +0 -0
  43. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/05_common_sense.py +0 -0
  44. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/06_causal_reasoning.py +0 -0
  45. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/07_logical_deduction.py +0 -0
  46. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/08_reading_comprehension.py +0 -0
  47. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/09_general_knowledge.py +0 -0
  48. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/10_implicit_reasoning.py +0 -0
  49. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/11_analogical_reasoning.py +0 -0
  50. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/examples/__init__.py +0 -0
  51. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/model_discovery.py +0 -0
  52. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/orchestrator.py +0 -0
  53. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/py.typed +0 -0
  54. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/shared_args.py +0 -0
  55. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/__init__.py +0 -0
  56. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/acp/SKILL.md +0 -0
  57. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/datetime/SKILL.md +0 -0
  58. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/loader.py +0 -0
  59. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/SKILL.md +0 -0
  60. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/__init__.py +0 -0
  61. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/aggregate_benchmark.py +0 -0
  62. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/generate_report.py +0 -0
  63. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/improve_description.py +0 -0
  64. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/init_skill.py +0 -0
  65. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/package_skill.py +0 -0
  66. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/quick_validate.py +0 -0
  67. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/run_eval.py +0 -0
  68. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/run_loop.py +0 -0
  69. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/security_scan.py +0 -0
  70. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/test_package_skill.py +0 -0
  71. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/test_quick_validate.py +0 -0
  72. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/utils.py +0 -0
  73. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/validate.py +0 -0
  74. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/skills/web-search/SKILL.md +0 -0
  75. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/soul/__init__.py +0 -0
  76. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/soul/loader.py +0 -0
  77. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/soul/types.py +0 -0
  78. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/souls/nova-helper/IDENTITY.md +0 -0
  79. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/souls/nova-helper/SOUL.md +0 -0
  80. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/souls/nova-helper/STYLE.md +0 -0
  81. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/souls/nova-helper/soul.json +0 -0
  82. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/tools/__init__.py +0 -0
  83. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova/tools/sandboxed_repl.py +0 -0
  84. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova.egg-info/dependency_links.txt +0 -0
  85. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova.egg-info/entry_points.txt +0 -0
  86. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova.egg-info/requires.txt +0 -0
  87. {agentnova-0.3.8 → agentnova-0.3.9}/agentnova.egg-info/top_level.txt +0 -0
  88. {agentnova-0.3.8 → agentnova-0.3.9}/localclaw/__init__.py +0 -0
  89. {agentnova-0.3.8 → agentnova-0.3.9}/localclaw-redirect/localclaw/__init__.py +0 -0
  90. {agentnova-0.3.8 → agentnova-0.3.9}/setup.cfg +0 -0
  91. {agentnova-0.3.8 → agentnova-0.3.9}/tests/test_agent.py +0 -0
  92. {agentnova-0.3.8 → agentnova-0.3.9}/tests/test_spec_compliance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentnova
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: ⚛️ AgentNova - A minimal, hackable agentic framework for local LLM inference
5
5
  Author-email: VTSTech <veritas@vts-tech.org>
6
6
  Maintainer-email: VTSTech <veritas@vts-tech.org>
@@ -32,7 +32,7 @@ Requires-Dist: black>=23.0; extra == "dev"
32
32
  Requires-Dist: ruff>=0.1.0; extra == "dev"
33
33
  Dynamic: license-file
34
34
 
35
- # ⚛️ AgentNova R03.8
35
+ # ⚛️ AgentNova R03.9
36
36
 
37
37
  **Status: Alpha**
38
38
 
@@ -77,12 +77,10 @@ Inspired by the architecture of OpenClaw, rebuilt from scratch for local-first o
77
77
  ## Installation
78
78
 
79
79
  ```bash
80
- # From source
81
- git clone https://github.com/VTSTech/AgentNova.git
82
- cd AgentNova
83
- pip install -e .
80
+ # Latest Development Release
81
+ pip install git+https://github.com/VTSTech/AgentNova.git --force-reinstall
84
82
 
85
- # Or from PyPI (when published)
83
+ # Last Stable (as stable as Alpha can be) Release
86
84
  pip install agentnova
87
85
  ```
88
86
 
@@ -1,4 +1,4 @@
1
- # ⚛️ AgentNova R03.8
1
+ # ⚛️ AgentNova R03.9
2
2
 
3
3
  **Status: Alpha**
4
4
 
@@ -43,12 +43,10 @@ Inspired by the architecture of OpenClaw, rebuilt from scratch for local-first o
43
43
  ## Installation
44
44
 
45
45
  ```bash
46
- # From source
47
- git clone https://github.com/VTSTech/AgentNova.git
48
- cd AgentNova
49
- pip install -e .
46
+ # Latest Development Release
47
+ pip install git+https://github.com/VTSTech/AgentNova.git --force-reinstall
50
48
 
51
- # Or from PyPI (when published)
49
+ # Last Stable (as stable as Alpha can be) Release
52
50
  pip install agentnova
53
51
  ```
54
52
 
@@ -28,7 +28,7 @@ Example Usage:
28
28
  agent = Agent(model="qwen2.5:0.5b", soul="/path/to/soul/package")
29
29
  """
30
30
 
31
- __version__ = "0.3.8"
31
+ __version__ = "0.3.9"
32
32
  __author__ = "VTSTech"
33
33
  __status__ = "Alpha"
34
34
 
@@ -226,7 +226,7 @@ class Agent:
226
226
  self.memory = Memory(memory_config or MemoryConfig())
227
227
 
228
228
  # Get model configuration (for temperature, max_tokens defaults)
229
- from .core.model_config import get_model_config
229
+ from .core.model_family_config import get_model_config
230
230
  self.model_config = get_model_config(model)
231
231
 
232
232
  # Detect model family (for backend-specific settings like think=False)
@@ -414,8 +414,11 @@ Final Answer: <the answer>
414
414
  # No synthesis or fallback mechanisms are used.
415
415
 
416
416
  # Track when we're expecting a Final Answer (after successful tool use)
417
+ # Only enforced for terminal tools (calculator, get_time, etc.) to avoid
418
+ # breaking multi-step workflows that need multiple tool calls.
417
419
  _expecting_final_answer = False
418
420
  _last_successful_result = None
421
+ _last_tool_name = None
419
422
 
420
423
  # Reset error tracker for new run
421
424
  self._error_tracker.reset()
@@ -584,9 +587,6 @@ Final Answer: <the answer>
584
587
  self.memory.add("user", f"Observation: Error: {error_msg}")
585
588
  continue
586
589
 
587
- # Fuzzy match tool name
588
- tool_name = self._parser._fuzzy_match_tool(tool_name)
589
-
590
590
  # Create FunctionCallItem
591
591
  fc_item = create_function_call_item(tool_name, tool_args, tool_call_id)
592
592
  fc_item.status = ItemStatus.IN_PROGRESS
@@ -616,6 +616,9 @@ Final Answer: <the answer>
616
616
  fc_item.status = ItemStatus.FAILED
617
617
  response.mark_failed({"message": "Too many tool failures", "type": "error_recovery"})
618
618
  break
619
+ else:
620
+ # Record success to reset consecutive failure counter
621
+ self._error_tracker.record_success(tool_name)
619
622
 
620
623
  # Update FunctionCallItem status
621
624
  fc_item.status = ItemStatus.COMPLETED
@@ -648,15 +651,25 @@ Final Answer: <the answer>
648
651
  )
649
652
 
650
653
  # Update expecting_final_answer flag
654
+ # Only enforce for terminal tools (simple, direct-answer tools)
655
+ # to avoid breaking multi-step workflows
651
656
  if is_error:
652
657
  _expecting_final_answer = False
658
+ _last_tool_name = None
653
659
  else:
654
- _expecting_final_answer = True
655
- _last_successful_result = str(result)
660
+ from .core.error_recovery import _is_simple_result
661
+ if _is_simple_result(str(result), tool_name):
662
+ _expecting_final_answer = True
663
+ _last_successful_result = str(result)
664
+ _last_tool_name = tool_name
665
+ else:
666
+ # Complex/intermediate result — allow more tool calls
667
+ _expecting_final_answer = False
668
+ _last_tool_name = None
656
669
 
657
670
  self.memory.add("user", observation_msg)
658
671
 
659
- if not str(result).startswith("Error"):
672
+ if not is_error:
660
673
  successful_results.append(f"{tool_name}: {result}")
661
674
 
662
675
  steps.append(StepResult(
@@ -956,6 +969,7 @@ Final Answer: <the answer>
956
969
  # Stream model output, check for tool calls, execute them, repeat.
957
970
  _expecting_final_answer = False
958
971
  _last_successful_result = None
972
+ _last_tool_name = None
959
973
  tool_call_count = 0
960
974
 
961
975
  for step_num in range(self.max_steps):
@@ -1030,9 +1044,6 @@ Final Answer: <the answer>
1030
1044
  self.memory.add("user", f"Observation: Error: {error_msg}")
1031
1045
  continue
1032
1046
 
1033
- # Fuzzy match
1034
- tool_name = self._parser._fuzzy_match_tool(tool_name)
1035
-
1036
1047
  # Create FunctionCallItem and emit SSE events
1037
1048
  fc_item = create_function_call_item(tool_name, tool_args)
1038
1049
  fc_item.status = ItemStatus.IN_PROGRESS
@@ -1075,9 +1086,16 @@ Final Answer: <the answer>
1075
1086
 
1076
1087
  if is_error:
1077
1088
  _expecting_final_answer = False
1089
+ _last_tool_name = None
1078
1090
  else:
1079
- _expecting_final_answer = True
1080
- _last_successful_result = str(result)
1091
+ from .core.error_recovery import _is_simple_result
1092
+ if _is_simple_result(str(result), tool_name):
1093
+ _expecting_final_answer = True
1094
+ _last_successful_result = str(result)
1095
+ _last_tool_name = tool_name
1096
+ else:
1097
+ _expecting_final_answer = False
1098
+ _last_tool_name = None
1081
1099
 
1082
1100
  self.memory.add("user", observation_msg)
1083
1101
 
@@ -1330,12 +1348,7 @@ Final Answer: <the answer>
1330
1348
  OpenResponses: Tool execution is straightforward - no synthesis.
1331
1349
  Arguments must come from the model.
1332
1350
  """
1333
- from .core.tool_parse import _fuzzy_match_tool_name
1334
- matched_name = _fuzzy_match_tool_name(name, self.tools.names())
1335
- if matched_name:
1336
- name = matched_name
1337
-
1338
- tool = self.tools.get_fuzzy(name)
1351
+ tool = self.tools.get(name)
1339
1352
 
1340
1353
  if tool is None:
1341
1354
  return f"Error: Unknown tool '{name}'. Available tools: {self.tools.names()}"
@@ -978,6 +978,9 @@ class OllamaBackend(BaseBackend):
978
978
  Key insight: "native" requires ACTUAL native tool_calls structure in API response.
979
979
  Models that output JSON as text are "react", not "native".
980
980
 
981
+ Results are automatically cached after each live test. If force_test is False,
982
+ the cache is checked first before returning UNTESTED.
983
+
981
984
  Args:
982
985
  model: Model name
983
986
  family: Optional family hint (unused, kept for API compatibility)
@@ -986,9 +989,16 @@ class OllamaBackend(BaseBackend):
986
989
  Returns:
987
990
  ToolSupportLevel (NATIVE, REACT, NONE, or UNTESTED if force_test=False)
988
991
  """
992
+ from ..core.tool_cache import get_cached_tool_support, cache_tool_support
993
+
994
+ # Determine API mode for cache namespacing
995
+ api_mode = self._api_mode.value if hasattr(self, "_api_mode") else "openre"
996
+
989
997
  if not force_test:
990
- # Without force_test, we don't know - return UNTESTED
991
- # The caller should use cache or show "untested"
998
+ # Check cache first, even without force_test
999
+ cached = get_cached_tool_support(model, api_mode=api_mode)
1000
+ if cached is not None:
1001
+ return cached
992
1002
  return ToolSupportLevel.UNTESTED
993
1003
 
994
1004
  # Test tool: Weather (simple, commonly supported)
@@ -1032,20 +1042,33 @@ class OllamaBackend(BaseBackend):
1032
1042
  if func_name == "get_weather":
1033
1043
  # Check for reasonable arguments
1034
1044
  if isinstance(args, dict) and ("location" in args or "city" in args or len(args) > 0):
1035
- return ToolSupportLevel.NATIVE
1045
+ result = ToolSupportLevel.NATIVE
1046
+ break
1036
1047
  # Native tool_calls exist but wrong function - still native capability
1037
1048
  # (model might be confused but HAS native support)
1038
1049
  elif func_name: # Any function name = native structure
1039
- return ToolSupportLevel.NATIVE
1050
+ result = ToolSupportLevel.NATIVE
1051
+ break
1052
+ else:
1053
+ # tool_calls existed but no valid function found
1054
+ result = ToolSupportLevel.REACT
1055
+ else:
1056
+ result = None # No native tool calls
1057
+
1058
+ if result is not None:
1059
+ cache_tool_support(model, result, family=family or "", api_mode=api_mode)
1060
+ return result
1040
1061
 
1041
1062
  # 3. Check for text-based tool calls in content
1042
1063
  # Models that output JSON like {"name": "get_weather", ...} as TEXT
1043
1064
  if self._contains_text_tool_call(content):
1065
+ cache_tool_support(model, ToolSupportLevel.REACT, family=family or "", api_mode=api_mode)
1044
1066
  return ToolSupportLevel.REACT
1045
1067
 
1046
1068
  # 4. API succeeded, no explicit rejection, no native tool_calls
1047
1069
  # Model accepted tools parameter but didn't use native calling
1048
1070
  # This is the "react" case - can still parse text-based tool calls
1071
+ cache_tool_support(model, ToolSupportLevel.REACT, family=family or "", api_mode=api_mode)
1049
1072
  return ToolSupportLevel.REACT
1050
1073
 
1051
1074
  except Exception as e:
@@ -1053,9 +1076,13 @@ class OllamaBackend(BaseBackend):
1053
1076
 
1054
1077
  # 1. Check for explicit "does not support tools" rejection
1055
1078
  if "does not support tools" in error_str.lower():
1079
+ cache_tool_support(model, ToolSupportLevel.NONE, family=family or "",
1080
+ error=error_str[:100], api_mode=api_mode)
1056
1081
  return ToolSupportLevel.NONE
1057
1082
 
1058
1083
  # Other HTTP errors or connection issues
1084
+ cache_tool_support(model, ToolSupportLevel.REACT, family=family or "",
1085
+ error=error_str[:100], api_mode=api_mode)
1059
1086
  return ToolSupportLevel.REACT
1060
1087
 
1061
1088
  finally:
@@ -6,9 +6,9 @@ that form the foundation of the AgentNova framework.
6
6
  """
7
7
 
8
8
  from .types import StepResultType, ToolSupportLevel, BackendType, ApiMode
9
- from .models import StepResult, AgentRun, Tool, ToolParam
9
+ from .models import StepResult, AgentRun, Tool, ToolParam, ToolCall
10
10
  from .memory import Memory, MemoryConfig
11
- from .tool_parse import ToolParser, ToolCall
11
+ from .tool_parse import ToolParser
12
12
  from .helpers import (
13
13
  fuzzy_match, normalize_args, validate_path, is_safe_url,
14
14
  strip_tool_prefix, is_simple_answered_query, is_greeting_or_simple,
@@ -19,18 +19,18 @@ from .prompts import (
19
19
  TOOL_ARG_ALIASES, FEW_SHOT_SUFFIX, FEW_SHOT_COMPACT,
20
20
  PLATFORM_DIR_CMD,
21
21
  )
22
- from .model_config import ModelFamilyConfig, get_model_config
23
- from .args_normal import (
24
- normalize_args as normalize_args_full,
25
- fix_calculator_args, synthesize_missing_args, generate_helpful_error_message,
26
- )
27
22
  from .model_family_config import (
23
+ ModelFamilyConfig, get_model_config,
28
24
  get_family_config, get_stop_tokens, supports_tools,
29
25
  get_tool_format, get_no_tools_system_prompt, get_preferred_temperature,
30
26
  should_use_few_shot, get_few_shot_style, has_known_issues,
31
27
  get_react_system_suffix, get_native_tool_hints,
32
28
  FAMILY_CONFIGS,
33
29
  )
30
+ from .args_normal import (
31
+ normalize_args as normalize_args_full,
32
+ fix_calculator_args, synthesize_missing_args, generate_helpful_error_message,
33
+ )
34
34
  # OpenResponses types
35
35
  from .openresponses import (
36
36
  Response, ResponseStatus, ItemStatus,
@@ -131,10 +131,40 @@ def normalize_args(args: dict[str, Any], expected_params: list[str], tool_name:
131
131
  power_parts = {}
132
132
 
133
133
  # Import tool-specific aliases
134
- from .prompts import TOOL_ARG_ALIASES
134
+ from .prompts import TOOL_ARG_ALIASES, CONTEXTUAL_ALIASES
135
135
 
136
136
  # Get tool-specific aliases
137
137
  tool_aliases = TOOL_ARG_ALIASES.get(tool_name, {}) if tool_name else {}
138
+ contextual_aliases = CONTEXTUAL_ALIASES.get(tool_name, set()) if tool_name else set()
139
+
140
+ # First pass: identify which keys map to expected params via non-ambiguous means
141
+ # (direct match, case-insensitive match, or non-contextual alias)
142
+ matched_keys = set()
143
+
144
+ for key, value in args.items():
145
+ key_lower = key.lower().replace("-", "_")
146
+
147
+ # Direct match
148
+ if key in expected_set:
149
+ matched_keys.add(key_lower)
150
+ continue
151
+
152
+ # Case-insensitive match
153
+ for param in expected_params:
154
+ if param.lower() == key_lower:
155
+ matched_keys.add(key_lower)
156
+ break
157
+
158
+ # Non-contextual alias (safe to apply always)
159
+ if key_lower not in matched_keys and key_lower in tool_aliases:
160
+ alias_target = tool_aliases[key_lower]
161
+ if alias_target != "_combine_power" and alias_target in expected_set:
162
+ if key_lower not in contextual_aliases:
163
+ matched_keys.add(key_lower)
164
+
165
+ # Second pass: build normalized dict with full resolution
166
+ normalized = {}
167
+ power_parts = {}
138
168
 
139
169
  for key, value in args.items():
140
170
  key_lower = key.lower().replace("-", "_")
@@ -148,8 +178,13 @@ def normalize_args(args: dict[str, Any], expected_params: list[str], tool_name:
148
178
  power_parts[key_lower] = value
149
179
  continue
150
180
  elif alias_target in expected_set:
151
- target_param = alias_target
152
- target_pname = alias_target
181
+ # Contextual validation: only apply ambiguous aliases when
182
+ # no other key has already matched an expected param
183
+ if key_lower in contextual_aliases and matched_keys:
184
+ pass # Skip this ambiguous alias
185
+ else:
186
+ target_param = alias_target
187
+ target_pname = alias_target
153
188
 
154
189
  # Strategy 2: Direct match
155
190
  if target_param is None and key in expected_set:
@@ -0,0 +1,36 @@
1
+ """
2
+ AgentNova — Model Configuration (DEPRECATED)
3
+
4
+ This module is deprecated. All model family configuration has been merged into
5
+ model_family_config.py. This module re-exports ModelFamilyConfig and
6
+ get_model_config from there for backward compatibility.
7
+
8
+ Please import from model_family_config instead:
9
+ from agentnova.core.model_family_config import ModelFamilyConfig, get_model_config
10
+
11
+ Written by VTSTech — https://www.vts-tech.org
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import warnings
17
+
18
+ # Re-export from the unified module for backward compatibility
19
+ from .model_family_config import ModelFamilyConfig, get_model_config
20
+
21
+ # Legacy MODEL_CONFIGS dict — maps to FAMILY_CONFIGS entries
22
+ from .model_family_config import FAMILY_CONFIGS as MODEL_CONFIGS
23
+
24
+
25
+ def list_supported_families() -> list[str]:
26
+ """List all supported model families."""
27
+ return [k for k in MODEL_CONFIGS.keys()]
28
+
29
+
30
+ # Emit deprecation warning on import
31
+ warnings.warn(
32
+ "model_config.py is deprecated. Use model_family_config.py instead. "
33
+ "Import ModelFamilyConfig and get_model_config from agentnova.core.model_family_config.",
34
+ DeprecationWarning,
35
+ stacklevel=2,
36
+ )
@@ -21,26 +21,50 @@ _PLATFORM_DIR_CMD = "cd" if _IS_WINDOWS else "pwd"
21
21
 
22
22
  @dataclass
23
23
  class ModelFamilyConfig:
24
- """Configuration for a specific model family."""
24
+ """Configuration for a specific model family.
25
+
26
+ Unified configuration combining prompting, formatting, tool handling,
27
+ and generation defaults. Single source of truth for all model-family-
28
+ specific settings.
29
+ """
25
30
  family: str
31
+ # Prompting & formatting
26
32
  start_tokens: dict = field(default_factory=dict)
27
33
  stop_tokens: list[str] = field(default_factory=list)
28
34
  tool_format: Literal["native", "xml", "json", "none"] = "native"
29
35
  tool_call_start: str = ""
30
36
  tool_call_end: str = ""
31
37
  supports_native_tools: bool = True
32
- system_prompt_style: Literal["separate", "first_user", "template"] = "separate"
38
+ system_prompt_style: Literal["separate", "first_user", "template", "default", "react", "minimal"] = "separate"
39
+ # Generation defaults
33
40
  preferred_temperature: float = 0.7
41
+ default_temperature: float = 0.7 # Used by agent.py for generation
42
+ default_top_p: float = 0.9
43
+ default_max_tokens: int = 8192
44
+ # Model behavior
34
45
  needs_think_directive: bool = False
35
46
  prefers_few_shot: bool = True
36
47
  few_shot_style: Literal["react", "native", "compact"] = "react"
37
48
  reasoning_hints: list[str] = field(default_factory=list)
49
+ # Streaming / capabilities
50
+ supports_streaming: bool = True
51
+ supports_vision: bool = False
52
+ # Think tag handling (e.g., <think/> for DeepSeek)
53
+ think_tag: str | None = None
54
+ strip_think_tags: bool = False
38
55
  # Special behaviors
39
56
  has_schema_dump_issue: bool = False # Some models dump tool schema as text
40
57
  truncate_json_args: bool = False # Some models truncate JSON in ReAct
58
+ needs_empty_system: bool = False # Some models break with empty system
59
+ prefers_user_system: bool = False # Put system prompt in first user message
41
60
  # Override system prompt for models without tool support (pure reasoning)
42
61
  no_tools_system_prompt: str | None = None
43
62
 
63
+ def get_stop_sequences(self) -> list[str]:
64
+ """Get stop sequences for this model family."""
65
+ stops = list(self.stop_tokens)
66
+ return stops
67
+
44
68
 
45
69
  # ═══════════════════════════════════════════════════════════════════════════════
46
70
  # FAMILY CONFIGURATIONS
@@ -57,6 +81,7 @@ FAMILY_CONFIGS: dict[str, ModelFamilyConfig] = {
57
81
  supports_native_tools=False,
58
82
  system_prompt_style="first_user",
59
83
  preferred_temperature=0.7,
84
+ needs_empty_system=True,
60
85
  prefers_few_shot=False,
61
86
  few_shot_style="compact",
62
87
  reasoning_hints=["Think step by step", "Show your work"],
@@ -262,6 +287,8 @@ Keep answers brief. One word when possible.""",
262
287
  preferred_temperature=0.6,
263
288
  needs_think_directive=True, # DeepSeek-R1 has thinking mode
264
289
  prefers_few_shot=True,
290
+ think_tag="think",
291
+ strip_think_tags=True,
265
292
  few_shot_style="react",
266
293
  reasoning_hints=["Think step by step", "Show your reasoning"],
267
294
  ),
@@ -284,6 +311,8 @@ Keep answers brief. One word when possible.""",
284
311
  needs_think_directive=False, # Standard DeepSeek models don't have thinking mode
285
312
  prefers_few_shot=True,
286
313
  few_shot_style="react",
314
+ think_tag="think",
315
+ strip_think_tags=True,
287
316
  ),
288
317
  }
289
318
 
@@ -441,6 +470,25 @@ def detect_family(model_name: str) -> str | None:
441
470
  return None
442
471
 
443
472
 
473
+ def get_model_config(model_name: str) -> ModelFamilyConfig:
474
+ """
475
+ Get unified configuration for a model.
476
+
477
+ This replaces the separate get_model_config() from model_config.py.
478
+ Uses detect_family() for consistent family resolution.
479
+
480
+ Args:
481
+ model_name: Name of the model (e.g., "qwen2.5:7b")
482
+
483
+ Returns:
484
+ ModelFamilyConfig for the model
485
+ """
486
+ family = detect_family(model_name)
487
+ if family and family in FAMILY_CONFIGS:
488
+ return FAMILY_CONFIGS[family]
489
+ return ModelFamilyConfig(family=family or "unknown")
490
+
491
+
444
492
  __all__ = [
445
493
  "ModelFamilyConfig",
446
494
  "FAMILY_CONFIGS",
@@ -456,5 +504,6 @@ __all__ = [
456
504
  "get_native_tool_hints",
457
505
  "get_no_tools_system_prompt",
458
506
  "detect_family",
507
+ "get_model_config",
459
508
  "needs_no_think_directive",
460
509
  ]
@@ -87,11 +87,16 @@ class Tool:
87
87
 
88
88
  @dataclass
89
89
  class ToolCall:
90
- """Represents a parsed tool call from model output."""
90
+ """Represents a parsed tool call from model output.
91
+
92
+ OpenResponses Enhancement: Includes thought capture for ReasoningItem.
93
+ """
91
94
  name: str
92
95
  arguments: dict[str, Any]
93
96
  raw: str = "" # Original text that was parsed
94
97
  confidence: float = 1.0 # Confidence of parsing (for fuzzy matches)
98
+ final_answer: str | None = None # OpenResponses: Final answer if present in same content
99
+ thought: str | None = None # OpenResponses: Captured reasoning for ReasoningItem
95
100
 
96
101
 
97
102
  @dataclass
@@ -25,7 +25,7 @@ TOOL_ARG_ALIASES = {
25
25
  "calculator": {
26
26
  # Common hallucinations for expression
27
27
  "a": "expression", "b": "expression", "x": "expression", "y": "expression",
28
- "num": "expression", "number": "expression", "value": "expression",
28
+ "num": "expression", "number": "expression",
29
29
  "input": "expression", "formula": "expression", "math": "expression",
30
30
  "expr": "expression", "calc": "expression", "result": "expression",
31
31
  # Power operations - combine into expression
@@ -89,6 +89,19 @@ TOOL_ARG_ALIASES = {
89
89
  },
90
90
  }
91
91
 
92
+ # Aliases that are too generic and should only be applied when the args dict
93
+ # has no other parameters that already matched a real expected param.
94
+ # These are the aliases most likely to misinterpret legitimate model output.
95
+ CONTEXTUAL_ALIASES = {
96
+ "calculator": {"value", "input", "result", "n", "p", "exp"},
97
+ "shell": {"text", "input", "arg", "args", "str", "value"},
98
+ "write_file": {"value", "string"},
99
+ "read_file": {"input"},
100
+ "web-search": {"text", "input"},
101
+ "python_repl": {"input", "expression", "expr"},
102
+ "convert_currency": {"value"},
103
+ }
104
+
92
105
  # ------------------------------------------------------------------ #
93
106
  # Few-shot prompting suffix for small models #
94
107
  # ------------------------------------------------------------------ #