master
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9ec578f229 |
feat(agents): add tool_agent_class parameter to AgentFactory and create_executor
CI / lint (pull_request) Successful in 44s
CI / typecheck (pull_request) Successful in 49s
CI / security (pull_request) Successful in 47s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m4s
CI / integration_tests (pull_request) Successful in 1m2s
CI / build (pull_request) Successful in 33s
CI / coverage (pull_request) Successful in 3m5s
CI / status-check (pull_request) Successful in 2s
CI / lint (push) Successful in 31s
CI / typecheck (push) Successful in 46s
CI / security (push) Successful in 46s
CI / quality (push) Successful in 31s
CI / unit_tests (push) Successful in 3m2s
CI / integration_tests (push) Successful in 1m2s
CI / build (push) Successful in 31s
CI / coverage (push) Successful in 3m4s
CI / status-check (push) Successful in 6s
Extends AgentFactory.__init__ and create_executor() with a keyword-only
tool_agent_class: type[ToolAgent] = ToolAgent parameter so callers can
inject a custom ToolAgent subclass at runtime without monkey-patching
module globals (resolves the five-module setattr loop in cleveragents-webapp).
**AgentFactory** (agents/factory.py):
- Accepts tool_agent_class keyword argument; validates it is a ToolAgent
subclass at construction time (fail-fast, CONTRIBUTING.md §Argument
Validation).
- Stores as self._tool_agent_class and uses it to populate
self.agent_types['tool'], so every create_agent('tool') call
instantiates the supplied subclass.
- Forwards tool_agent_class to LLMAgent when creating 'llm'-typed agents
so the LLM tool-calling loop honours the injected subclass.
- Default ToolAgent is preserved, so all existing callers are unaffected.
**LLMAgent** (agents/llm.py):
- Accepts a keyword-only tool_agent_class: type[ToolAgent] = ToolAgent
argument, validated as a ToolAgent subclass at construction time.
- The multi-turn tool-call loop (_execute_tool_loop) constructs its
ephemeral tool executors via self._tool_agent_class(...) across all
three dispatch sites (regular loop, budget-exhaustion synthesis round,
and stuck-model synthesis round), so the LLM tool-calling path no
longer falls back to the module-level ToolAgent. This closes the gap
that previously prevented removing the webapp monkey-patch for the LLM
tool-calling path.
**Executor / create_executor** (runtime.py):
- Executor.__init__ accepts and validates tool_agent_class; stores as
self.tool_agent_class.
- create_executor() accepts tool_agent_class and forwards it to Executor.
**Dispatch paths** (runtime_dispatch.py):
- _execute_tool: uses executor.tool_agent_class(...) instead of the
module-level ToolAgent(...) so the direct-construction path (bypassing
AgentFactory) also honours the injected subclass.
- _execute_graph and _execute_graph_stream: pass
tool_agent_class=executor.tool_agent_class to AgentFactory so graph
nodes using tool-typed agents pick up the subclass.
- _execute_llm and _execute_llm_stream: pass
tool_agent_class=executor.tool_agent_class to AgentFactory so the
single-LLM actor's internally-created LLMAgent (and its tool-calling
loop) honours the injected subclass. Without this, a single LLM actor
that makes tool calls silently fell back to the base ToolAgent.
- _execute_multi_actor: passes tool_agent_class=executor.tool_agent_class
to the sub-Executor so every actor inside a multi-actor bundle (tool,
graph, or LLM) honours the injected subclass. Without this, all
dispatch paths reachable through a multi-actor bundle fell back to the
base ToolAgent.
- Removed the now-unused module-level ToolAgent import; the _execute_tool
docstring cross-reference is fully qualified so it still resolves.
All six dispatch paths that construct or dispatch tool agents now
forward the injected subclass.
**ReactiveCleverAgentsApp** (core/application.py):
- Removed the redundant register_agent_type('tool', ToolAgent) call from
load_configuration and the dead type_map re-registration block in
_create_agents; AgentFactory.__init__ already owns the 'tool'
registration. This drops the last hardcoded module-level ToolAgent
references flagged in the issue #73 audit.
**Tests** (features/tool_agent_class_injection.feature + steps):
- 17 BDD scenarios covering: default class when parameter omitted;
custom subclass stored on factory and executor; invalid class rejected
with ConfigurationError (factory, executor, and LLMAgent); factory
create_agent returns custom instance; _execute_tool dispatch
instantiates custom subclass; the LLM tool loop instantiates the
injected subclass; isinstance LSP compatibility through the factory;
the _execute_llm dispatch path forwarding tool_agent_class to
AgentFactory (single-LLM actor making a tool call); and the
_execute_multi_actor dispatch path forwarding tool_agent_class to the
sub-Executor. The two new dispatch-path scenarios were verified to
fail without the source fixes (instantiation_count=0), confirming
they guard the injection contract for the single-LLM and multi-actor
paths.
**Updated tests** (features/steps/runtime_coverage_steps.py,
features/steps/runtime_extended_coverage_steps.py):
- Removed the now-dead patch('cleveractors.runtime_dispatch.ToolAgent')
mocks (and their mock instances) that no longer intercept _execute_tool
after it switched to executor.tool_agent_class. The tool-actor scenario
continues to pass via the real ToolAgent; the normal-coverage branch
uses patch.object(ToolAgent, 'process_message', ...) to stub tool
execution.
**Updated test** (features/steps/credential_executor_paths_steps.py):
- The _execute_multi_actor credentials-forwarding scenario intercepts
Executor.__init__ with a capturing_init to assert the credentials
dict reaches the sub-Executor. Now that _execute_multi_actor forwards
tool_agent_class to the sub-Executor, capturing_init accepts and
forwards that keyword so the assertion continues to pass.
**Removed obsolete test** (features/application_coverage_gaps.feature):
- Dropped the '_create_agents registers built-in agent types' scenario
and its step definitions; the behaviour it asserted (redundant
re-registration) was removed from _create_agents.
ISSUES CLOSED: #73
|
||
|
|
8f986c1e31
|
test(coverage): add coverage scenarios for pre-existing code paths
Additional BDD scenarios covering registry resolver errors, cache TTL expiry, runtime dispatch normalization, template base edge cases, validation actor coverage gaps, and YAML Jinja loader deferred rendering. |
||
|
|
2664ebfd75 |
feat(ActorResult): implement ActorResult and NodeUsage types; capture per-node token counts from LangChain responses
CI / lint (pull_request) Successful in 1m9s
CI / typecheck (pull_request) Successful in 1m10s
CI / security (pull_request) Successful in 1m8s
CI / quality (pull_request) Successful in 44s
CI / build (pull_request) Successful in 45s
CI / integration_tests (pull_request) Successful in 1m15s
CI / unit_tests (pull_request) Successful in 3m48s
CI / coverage (pull_request) Successful in 3m43s
CI / status-check (pull_request) Successful in 3s
CI / quality (push) Successful in 44s
CI / lint (push) Successful in 49s
CI / security (push) Successful in 47s
CI / typecheck (push) Successful in 48s
CI / build (push) Successful in 50s
CI / integration_tests (push) Successful in 1m16s
CI / unit_tests (push) Successful in 3m50s
CI / coverage (push) Successful in 3m35s
CI / status-check (push) Successful in 3s
AC1: Move NodeUsage and ActorResult from runtime.py into the spec-mandated
cleveractors/result.py module. runtime.py now imports and re-exports both
types for backward compatibility. result.py defines __all__, uses
dict[str, Any] | None (PEP 585) for the state field, and makes nodes a
required positional field (no default) so the contract that nodes is always
non-empty is statically enforced.
AC2: LLMAgent.process_message() extracts real token usage from
response.usage_metadata (primary) with isinstance(dict) guard, falling back
to response.response_metadata['token_usage'] with its own isinstance(dict)
guard. If neither is available, logs a WARNING with a cause parameter that
distinguishes the three failure conditions (empty usage_metadata, missing
response_metadata, or missing token_usage key) so operators can triage billing
discrepancies. Non-numeric token values are coerced via _safe_int() with
fallback to 0 and a warning log.
AC3: Token counts are available alongside the response string via the
side-channel attribute LLMAgent._last_token_usage, keeping process_message()
return type as str (preserving the Agent base-class contract unchanged).
AC4: Node._execute_agent() reads _last_token_usage, provider, and model from
any LLMAgent after execution and includes a _node_token_usage dict in the
state-updates return. PureLangGraph._node_usages accumulates one
(node_id, provider, model, prompt_tokens, completion_tokens) 5-tuple per LLM
node invocation during _execute_from_node(). PureLangGraph.execute() now
returns a 3-tuple: (response, final_state, node_usages). process_message()
unpacks and discards the extra elements.
AC5: Executor._execute_graph() uses the node_usages list from execute() to
build NodeUsage objects. Executor._execute_llm() reads _last_token_usage
directly from the LLMAgent instance instead of calling _estimate_tokens().
State isolation verified by a new test that calls execute() twice on the same
Executor with different mocked token counts and asserts independent results.
AC6: Aggregation invariant enforced in both _execute_llm() and _execute_graph()
via sum() over nodes list.
AC7: ActorResult and NodeUsage re-exported from cleveractors/__init__.py via
cleveractors.result (path updated from cleveractors.runtime).
Cleanup: Delete runtime_tokens.py (estimate_tokens, estimate_graph_tokens) and
the private _estimate_tokens() function in runtime.py — both are superseded by
real LangChain usage_metadata extraction. Remove unused import logging and
logger = logging.getLogger(__name__) from runtime.py (dead code after dispatch
logic was moved to runtime_dispatch.py). Move NodeUsageTuple type alias in
pure_graph.py to after the import blocks. Use tuple unpacking for last_usage
in runtime_dispatch._execute_llm().
New/updated BDD tests (actor_result_token_counting.feature, 25 scenarios):
- AC2: usage_metadata primary path (input_tokens/output_tokens)
- AC2: truthy non-dict usage_metadata treated as absent (isinstance guard)
- AC2: truthy non-dict response_metadata treated as absent (isinstance guard)
- AC2: response_metadata={} (empty dict) logs warning, counts 0
- AC2: response_metadata fallback path (prompt_tokens/completion_tokens)
- AC2: no usage data -> warning logged, counts 0
- AC2: response_metadata attribute present but None
- AC2: stale token reset before each call
- AC5: state isolation — execute() twice produces independent results
- AC6: aggregation invariant for single-node and multi-node actors
- AC6: placeholder node_id exact format assertion for _execute_graph
- AC6: placeholder node_id exact format assertion for _execute_multi_actor
- AC7: import correctness from cleveractors and cleveractors.result
CHANGELOG.md updated with Changed (real token extraction, result module move)
and Removed (runtime_tokens.py) entries under [Unreleased].
Quality gates: lint pass, typecheck 0 errors, unit_tests 2157/2157,
integration_tests 86/86, coverage 97.2% (threshold 97%).
ISSUES CLOSED: #14
|
||
|
|
76c4c74201 |
feat(create_executor): implement create_executor() factory and Executor.execute() returning ActorResult
CI / lint (pull_request) Successful in 36s
CI / quality (pull_request) Successful in 40s
CI / build (pull_request) Successful in 44s
CI / security (pull_request) Successful in 51s
CI / typecheck (pull_request) Successful in 52s
CI / integration_tests (pull_request) Successful in 1m3s
CI / unit_tests (pull_request) Successful in 3m38s
CI / coverage (pull_request) Successful in 3m36s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 1m2s
CI / typecheck (push) Successful in 1m2s
CI / security (push) Successful in 1m2s
CI / quality (push) Successful in 40s
CI / integration_tests (push) Successful in 1m8s
CI / build (push) Successful in 49s
CI / unit_tests (push) Successful in 3m40s
CI / coverage (push) Successful in 3m36s
CI / status-check (push) Successful in 3s
- **`src/cleveractors/runtime_types.py`** — `ActorResult` and `NodeUsage` dataclasses
extracted to break circular imports (CONTRIBUTING.md §Import Guidelines). 100% coverage.
- **`src/cleveractors/runtime.py`** — `create_executor()` factory, `Executor` class
with `execute()` dispatching to module-level functions in `runtime_dispatch`.
Imports `_execute_*` at module level (no function-local imports). 100% coverage.
- **`src/cleveractors/runtime_dispatch.py`** — Four dispatch functions: `_execute_llm`,
`_execute_graph`, `_execute_tool`, `_execute_multi_actor`. All imports at module
level except `from cleveractors.runtime import Executor` inside `_execute_multi_actor`
(the only remaining circular dep — Executor calls _execute_multi_actor which creates
a new Executor; cannot be resolved without further restructuring). 99.69% coverage
(only the `if TYPE_CHECKING:` guard line is uncovered — not a real gap).
- **`src/cleveractors/runtime_tokens.py`** — Token estimation via tiktoken with
heuristic fallback. 100% coverage.
Key design decisions and fixes:
- **Circular import resolution**: `ActorResult`/`NodeUsage` moved to `runtime_types.py`;
both `runtime.py` and `runtime_dispatch.py` import from it at module level.
- **`messages` forwarded to `_execute_llm`**: builds `context={"conversation_history":[...]}`
passed to `LLMAgent.process_message` for multi-turn support.
- **`parallel_execution` aligned with `PureGraphConfig` default (True)**:
reads from legacy `route` dict or v2.0 `routes.main` dict; defaults to True.
- **Factory normalization**: always merges `actors` into `agents` (actors take
precedence) so configs using both keys work correctly.
- **Generic exception in agent creation re-raised as `ConfigurationError`**:
no double-logging; exception message preserved in `ConfigurationError`.
- **`cleveragents_block` guarded against `None`**: uses `or {}` coercion.
- **Type annotations added**: `top_provider`, `top_model`, `top_sp`,
`temperature_raw`, `max_tokens_raw`, `config_block`, `tools` in dispatch functions.
- **No `finally` in `_execute_tool`**: documented with comment (ToolAgent has no cleanup).
- **Dead step removed**: `step_rxe_invalid_node_type` was unreferenced and incorrect.
- **BDD coverage**: `parallel_execution=False` override verified via `PureGraphConfig`
constructor args; conversation history forwarding verified; AC7 immutability for
multi-actor path verified.
- ✅ `nox -e lint` — passes
- ✅ `nox -e format` — passes
- ✅ `nox -e typecheck` — passes (0 errors, 1 expected warning)
- ✅ `nox -e unit_tests` — 2113 scenarios pass, 0 failures, 0 skipped
- ✅ `nox -e integration_tests` — 76 tests pass
- ✅ `nox -e coverage_report` — 97.21% (9870 covered, 283 missing, 10153 total)
PR-modified files: `runtime_types.py` 100%, `runtime.py` 100%,
`runtime_dispatch.py` 99.69% (1 uncoverable TYPE_CHECKING guard), `runtime_tokens.py` 100%
ISSUES CLOSED: #13
|
||
|
|
3fc0a3fa57
|
test(coverage): add BDD unit tests to close coverage gap toward 97%
CI / lint (pull_request) Successful in 35s
CI / quality (pull_request) Successful in 44s
CI / typecheck (pull_request) Successful in 47s
CI / security (pull_request) Successful in 56s
CI / unit_tests (pull_request) Successful in 3m33s
CI / integration_tests (pull_request) Successful in 56s
CI / build (pull_request) Successful in 34s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / quality (push) Successful in 49s
CI / lint (push) Successful in 51s
CI / typecheck (push) Successful in 52s
CI / build (push) Successful in 41s
CI / security (push) Successful in 58s
CI / integration_tests (push) Successful in 1m0s
CI / unit_tests (push) Successful in 3m37s
CI / coverage (push) Successful in 3m34s
CI / status-check (push) Successful in 5s
Add comprehensive BDD Behave unit tests across six modules to improve line coverage. New feature files and step definitions for: - validation/_actor: graph/llm/tool/multi_actor config validation - runtime_tokens: token estimation with tiktoken and fallback heuristics - dynamic_router: ContentBasedCondition, RouterNode, graph extension - runtime: v2 route format, config blocks, conversation history, multi-actor - progress: ProgressBarManager remaining count resolution - route: MESSAGE_ROUTER rules to metadata in to_graph_config Also fix credential executor and graph cleanup step mocks to return proper tuple values matching updated runtime signatures. Refs: #39 |
||
|
|
f281fa3b24 |
feat(credentials): refactor LLMAgent/AgentFactory for per-request credential injection and extended provider routing
CI / lint (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 50s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 32s
CI / unit_tests (pull_request) Successful in 3m32s
CI / integration_tests (pull_request) Successful in 59s
CI / build (pull_request) Successful in 34s
CI / lint (push) Successful in 33s
CI / build (push) Successful in 37s
CI / quality (push) Successful in 48s
CI / typecheck (push) Successful in 48s
CI / security (push) Successful in 1m4s
CI / integration_tests (push) Successful in 1m18s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / unit_tests (push) Successful in 3m45s
CI / coverage (push) Successful in 3m34s
CI / status-check (push) Successful in 3s
Implements Wave 3 of the cleveractors-core integration (ADR-2026, ADR-2028). ## AgentFactory - Added optional credentials: dict[str, dict[str, str]] | None parameter to __init__. Stored as self.credentials. - When creating LLMAgent instances, the full credentials dict is forwarded via a new credentials constructor kwarg, threading per-request API keys without touching the stored actor config_dict. - Added explicit type annotation ReactiveAgentFactory: type[AgentFactory]. - _create_agent_instance raises ConfigurationError when credentials is not None but does not contain an entry for the requested provider (AC4 compliance). ## LLMAgent - Added optional credentials: dict[str, str] | None parameter to __init__. Stored as self._credentials; config_dict is never modified. - LangChain client construction deferred to first access (lazy init) via a chat_model property backed by _ensure_chat_model(). The property setter allows test-injection of mock models without going through the lazy-init path. - _create_chat_model() dispatches to credential-injection / standalone paths. - Extracted to llm_client.py and llm_providers.py for modularity. - Defined shared module-level constants DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS to eliminate duplicated magic numbers across llm.py, runtime.py, and factory.py. - Added public credentials property exposing self._credentials. - Added type narrowing guard in chat_model property to satisfy static analysis. - Added temperature_override type validation in process_message. - Guarded cleanup() null-assignment with _chat_model_lock (TOCTOU fix). ## SSRF Prevention (ADR-2028) - _validate_base_url(): https-only, userinfo rejection, localhost rejection, raw IP literal rejection, numeric-hostname (dotted-hex/octal/compact) rejection, percent-decode loop with iteration cap (10), trailing-dot FQDN strip. - DNS resolution intentionally omitted (blocking call in async path). - Canonical URL reconstruction uses lowercased scheme. ## Credential Leak Prevention - All logger.exception(...) replaced with logger.debug(..., exc_info=True). - Error messages use type(e).__name__ instead of str(e). - process_message exception chains broken with from None to prevent LangChain authentication errors from leaking via __cause__. ## Runtime fixes - Executor._execute_llm: shallow copy for setdefault mutations. - Executor._execute_graph: restructured try/finally for resource cleanup. - Uses DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS from llm.py. - Defensive copy of credentials dict in Executor.__init__ to prevent caller mutation after construction. ## Test improvements - Added BDD scenarios for llm_imports.py ImportError fallback branches using sys.modules manipulation with a custom import hook, exercised via subprocess. - Added BDD scenario for AgentFactory.create_agents_from_config() credential threading to all created agents. - Removed tautological dataclass test scenarios (NodeUsage, ActorResult) from runtime_coverage.feature. - Fixed misleading scenario title in runtime_coverage.feature (exception chain suppressed, not chained) and updated step to assert __cause__ is None. - Added CHANGELOG entry for ReactiveAgentFactory backward-compatibility alias. - Removed duplicate log emission in _check_known_provider_domain. ISSUES CLOSED: #12 |
||
|
|
ab17407576
|
test(runtime): add BDD coverage tests for runtime Executor API
CI / lint (pull_request) Successful in 40s
CI / typecheck (pull_request) Successful in 58s
CI / security (pull_request) Successful in 1m8s
CI / quality (pull_request) Successful in 1m7s
CI / integration_tests (pull_request) Successful in 1m20s
CI / build (pull_request) Successful in 39s
CI / unit_tests (pull_request) Successful in 3m47s
CI / coverage (pull_request) Successful in 3m44s
CI / status-check (pull_request) Successful in 4s
CI / quality (push) Successful in 39s
CI / lint (push) Successful in 47s
CI / security (push) Successful in 52s
CI / integration_tests (push) Successful in 1m2s
CI / typecheck (push) Successful in 1m5s
CI / build (push) Successful in 1m11s
CI / unit_tests (push) Successful in 4m27s
CI / coverage (push) Successful in 4m28s
CI / status-check (push) Successful in 5s
Add 16 Behave BDD scenarios covering: - create_executor() factory function and Executor initialization - Executor.execute() dispatch to LLM, graph, tool, multi-actor agents - _execute_llm credential injection and error handling - _execute_graph PureGraphConfig construction - _execute_tool tool configuration - _execute_multi_actor routing and error cases - _build_factory_config credential/limit injection - _estimate_tokens with tiktoken and fallback heuristic - NodeUsage and ActorResult dataclasses ISSUES CLOSED: #33 Refs: #33 |