sofias-sdk-lite 0.1.4__tar.gz → 0.1.5__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.

Potentially problematic release.


This version of sofias-sdk-lite might be problematic. Click here for more details.

Files changed (80) hide show
  1. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/PKG-INFO +1 -1
  2. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/pyproject.toml +1 -1
  3. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/pyproject.toml.orig +1 -1
  4. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/llm_node.py +67 -0
  5. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/tools.py +39 -2
  6. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/README.md +0 -0
  7. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/__init__.py +0 -0
  8. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/agent/__init__.py +0 -0
  9. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/agent/agent.py +0 -0
  10. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/agent/agent_builder.py +0 -0
  11. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/agent/agent_config.py +0 -0
  12. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/agent/execution_context.py +0 -0
  13. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/agent/middleware.py +0 -0
  14. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/agent/response_workflow.py +0 -0
  15. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/agent/streaming.py +0 -0
  16. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/config/__init__.py +0 -0
  17. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/config/agent_settings.py +0 -0
  18. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/config/defaults.py +0 -0
  19. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/config/runtime_config.py +0 -0
  20. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/config/sdk_config.py +0 -0
  21. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/config/source.py +0 -0
  22. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/contracts/__init__.py +0 -0
  23. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/contracts/agent_contracts.py +0 -0
  24. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/contracts/base_contracts.py +0 -0
  25. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/contracts/node_contracts.py +0 -0
  26. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/contracts/strict_contract.py +0 -0
  27. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/errors/__init__.py +0 -0
  28. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/errors/circuit_breaker.py +0 -0
  29. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/errors/error_handler.py +0 -0
  30. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/errors/exceptions.py +0 -0
  31. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/llm/__init__.py +0 -0
  32. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/llm/bifrost.py +0 -0
  33. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/llm/factory.py +0 -0
  34. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/llm/openai_compatible.py +0 -0
  35. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/llm/protocol.py +0 -0
  36. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/llm/tokens.py +0 -0
  37. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/llm/tools.py +0 -0
  38. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/messaging/__init__.py +0 -0
  39. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/messaging/models.py +0 -0
  40. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/__init__.py +0 -0
  41. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/aggregator_config.py +0 -0
  42. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/aggregator_node.py +0 -0
  43. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/base_node.py +0 -0
  44. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/delegation_config.py +0 -0
  45. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/delegation_node.py +0 -0
  46. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/delegation_transport.py +0 -0
  47. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/function_node.py +0 -0
  48. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/llm_node_config.py +0 -0
  49. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/plan_executor.py +0 -0
  50. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/planner_node.py +0 -0
  51. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/planning_models.py +0 -0
  52. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/nodes/prompt_assembler.py +0 -0
  53. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/observability/__init__.py +0 -0
  54. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/observability/_log.py +0 -0
  55. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/observability/events.py +0 -0
  56. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/observability/trace_context.py +0 -0
  57. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/observability/tracing.py +0 -0
  58. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/py.typed +0 -0
  59. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/rabbitmq/__init__.py +0 -0
  60. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/rabbitmq/client.py +0 -0
  61. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/rabbitmq/config.py +0 -0
  62. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/rabbitmq/consumer.py +0 -0
  63. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/rabbitmq/delegation_transport.py +0 -0
  64. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/rabbitmq/publisher.py +0 -0
  65. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/rabbitmq/rpc_client.py +0 -0
  66. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/rabbitmq/types.py +0 -0
  67. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/routing/__init__.py +0 -0
  68. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/routing/router.py +0 -0
  69. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/routing/strategies.py +0 -0
  70. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/runner/__init__.py +0 -0
  71. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/runner/config.py +0 -0
  72. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/runner/runner.py +0 -0
  73. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/state/__init__.py +0 -0
  74. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/state/conversation_state.py +0 -0
  75. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/state/history.py +0 -0
  76. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/state/memory.py +0 -0
  77. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/workflows/__init__.py +0 -0
  78. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/workflows/chat.py +0 -0
  79. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/workflows/null.py +0 -0
  80. {sofias_sdk_lite-0.1.4 → sofias_sdk_lite-0.1.5}/src/sofias_sdk_lite/workflows/streaming_chat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sofias-sdk-lite
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Build agents on the Sofias agent graph and run them over RabbitMQ.
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Dist: pydantic>=2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofias-sdk-lite"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Build agents on the Sofias agent graph and run them over RabbitMQ."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofias-sdk-lite"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Build agents on the Sofias agent graph and run them over RabbitMQ."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -580,6 +580,61 @@ class LLMNode(BaseNode):
580
580
  all_specs = static_specs + registry_specs
581
581
  return all_specs if all_specs else None
582
582
 
583
+ def _tool_catalog_revision(self) -> int:
584
+ """Cheap read of the tool catalog's state.
585
+
586
+ Returns:
587
+ An integer that moves when the catalog may have changed; 0 when
588
+ the registry does not keep count (custom registries, test doubles).
589
+ """
590
+ rev = getattr(self._tool_registry, "revision", 0)
591
+ return rev if isinstance(rev, int) else 0
592
+
593
+ async def _refresh_tool_catalog(
594
+ self,
595
+ seen_revision: int,
596
+ tool_specs: list[ToolSpec] | None,
597
+ base_prompt: str,
598
+ prompt: str,
599
+ runtime_vars: dict[str, Any],
600
+ ) -> tuple[int, list[ToolSpec] | None, str]:
601
+ """Re-read the tools when the catalog changed mid-turn.
602
+
603
+ Specs used to be resolved once, before the loop, so a tool could never
604
+ widen the catalog for later iterations: the model never saw what it
605
+ had just loaded. For callers that do not use this the cost is one
606
+ integer comparison per iteration.
607
+
608
+ Args:
609
+ seen_revision: Revision the current specs were resolved at.
610
+ tool_specs: Current specs.
611
+ base_prompt: Prompt as the node's assembler produced it, before
612
+ the registry rebuild. Re-assembling the already rebuilt one
613
+ would duplicate the schemas inside the text.
614
+ prompt: Current prompt.
615
+ runtime_vars: Template variables for this turn.
616
+
617
+ Returns:
618
+ Updated ``(revision, specs, prompt)``, or the ones received when
619
+ nothing changed.
620
+ """
621
+ revision = self._tool_catalog_revision()
622
+ if revision == seen_revision:
623
+ return seen_revision, tool_specs, prompt
624
+
625
+ new_specs = await self._get_merged_tool_specs()
626
+ logger.info(
627
+ "Tool catalog refreshed",
628
+ node_name=self.name,
629
+ tools_before=len(tool_specs) if tool_specs else 0,
630
+ tools_after=len(new_specs) if new_specs else 0,
631
+ )
632
+ return (
633
+ revision,
634
+ new_specs,
635
+ self._prompt_with_registry_tools(base_prompt, new_specs, runtime_vars),
636
+ )
637
+
583
638
  def _prompt_with_registry_tools(
584
639
  self,
585
640
  prompt: str,
@@ -928,7 +983,9 @@ class LLMNode(BaseNode):
928
983
  max_iterations = self._get_max_iterations()
929
984
  parallel_tool_calls = self._get_parallel_tool_calls()
930
985
  tool_specs = await self._get_merged_tool_specs()
986
+ tool_catalog_revision = self._tool_catalog_revision()
931
987
 
988
+ base_prompt = prompt
932
989
  prompt = self._prompt_with_registry_tools(prompt, tool_specs, runtime_vars)
933
990
 
934
991
  history: list[tuple[LLMResponse, list[ToolResult]]] = []
@@ -940,6 +997,10 @@ class LLMNode(BaseNode):
940
997
  all_skipped_batches = 0
941
998
 
942
999
  while iteration < max_iterations:
1000
+ tool_catalog_revision, tool_specs, prompt = await self._refresh_tool_catalog(
1001
+ tool_catalog_revision, tool_specs, base_prompt, prompt, runtime_vars
1002
+ )
1003
+
943
1004
  if token_budget and history:
944
1005
  history, fits = await self._fit_history_to_budget(
945
1006
  prompt, tool_specs, history, token_budget, token_reserve
@@ -1417,7 +1478,9 @@ class LLMNode(BaseNode):
1417
1478
  max_iterations = self._get_max_iterations()
1418
1479
  parallel_tool_calls = self._get_parallel_tool_calls()
1419
1480
  tool_specs = await self._get_merged_tool_specs()
1481
+ tool_catalog_revision = self._tool_catalog_revision()
1420
1482
 
1483
+ base_prompt = prompt
1421
1484
  prompt = self._prompt_with_registry_tools(prompt, tool_specs, runtime_vars)
1422
1485
 
1423
1486
  history: list[tuple[LLMResponse, list[ToolResult]]] = []
@@ -1440,6 +1503,10 @@ class LLMNode(BaseNode):
1440
1503
  )
1441
1504
 
1442
1505
  while iteration < max_iterations:
1506
+ tool_catalog_revision, tool_specs, prompt = await self._refresh_tool_catalog(
1507
+ tool_catalog_revision, tool_specs, base_prompt, prompt, runtime_vars
1508
+ )
1509
+
1443
1510
  if token_budget and history:
1444
1511
  history, fits = await self._fit_history_to_budget(
1445
1512
  prompt, tool_specs, history, token_budget, token_reserve
@@ -336,6 +336,12 @@ class ToolProvider(Protocol):
336
336
 
337
337
  Implement this against whatever tool system you want to expose to an
338
338
  agent (MCP servers, a plugin registry, standalone functions, ...).
339
+
340
+ Optional member ``revision: int``: a provider whose catalog can change
341
+ mid-turn (e.g. a tool that loads a connector's tools on demand) may expose
342
+ a ``revision`` property that increments on every change. The node reads it
343
+ between tool-loop iterations and only then re-fetches the specs. Providers
344
+ that do not expose it behave exactly as before.
339
345
  """
340
346
 
341
347
  async def get_tools(self, node_name: str) -> list[ToolSpec]:
@@ -358,21 +364,45 @@ class ToolRegistry:
358
364
  self._provider = provider
359
365
  self._cache_tools = cache_tools
360
366
  self._tools_cache: dict[str, list[ToolSpec]] = {}
367
+ self._cached_at_revision: dict[str, int] = {}
368
+ self._revision = 0
361
369
 
362
370
  @property
363
371
  def provider(self) -> ToolProvider:
364
372
  """The underlying tool provider."""
365
373
  return self._provider
366
374
 
375
+ @property
376
+ def revision(self) -> int:
377
+ """Counter that moves whenever the catalog may have changed.
378
+
379
+ The node reads it between tool-loop iterations: while it stays put the
380
+ node reuses the specs it already has, so an agent that never touches
381
+ its catalog pays nothing. It moves on ``clear_cache`` and also adds the
382
+ provider's own ``revision`` when exposed (a cacheless provider can
383
+ change tools without going through ``clear_cache``). Only comparing
384
+ two reads is meaningful.
385
+ """
386
+ provider_revision = getattr(self._provider, "revision", 0)
387
+ return self._revision + (provider_revision if isinstance(provider_revision, int) else 0)
388
+
367
389
  async def get_tools_for_node(self, node_name: str) -> list[ToolSpec]:
368
390
  """Get tools available for a specific node, caching if enabled."""
369
- if self._cache_tools and node_name in self._tools_cache:
391
+ # A revision different from the one cached at means the provider
392
+ # changed its catalog on its own: the cached list is stale.
393
+ revision = self.revision
394
+ if (
395
+ self._cache_tools
396
+ and node_name in self._tools_cache
397
+ and self._cached_at_revision.get(node_name) == revision
398
+ ):
370
399
  return self._tools_cache[node_name]
371
400
 
372
401
  try:
373
402
  tools = await self._provider.get_tools(node_name)
374
403
  if self._cache_tools:
375
404
  self._tools_cache[node_name] = tools
405
+ self._cached_at_revision[node_name] = revision
376
406
  logger.debug(
377
407
  "Loaded tools for node",
378
408
  node_name=node_name,
@@ -396,11 +426,18 @@ class ToolRegistry:
396
426
  raise
397
427
 
398
428
  def clear_cache(self, node_name: str | None = None) -> None:
399
- """Clear the tools cache, for one node or entirely."""
429
+ """Clear the tools cache, for one node or entirely.
430
+
431
+ Moves ``revision``, which is how the node learns the catalog changed
432
+ and re-fetches the specs on its next iteration.
433
+ """
400
434
  if node_name is not None:
401
435
  self._tools_cache.pop(node_name, None)
436
+ self._cached_at_revision.pop(node_name, None)
402
437
  else:
403
438
  self._tools_cache.clear()
439
+ self._cached_at_revision.clear()
440
+ self._revision += 1
404
441
 
405
442
  def get_cached_tools(self, node_name: str) -> list[ToolSpec] | None:
406
443
  """Get cached tools for a node without fetching."""