agent-learning 0.4.2__tar.gz → 0.5.0__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.
- {agent_learning-0.4.2/src/agent_learning.egg-info → agent_learning-0.5.0}/PKG-INFO +13 -5
- agent_learning-0.5.0/PYPI.md +23 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/README.md +23 -12
- {agent_learning-0.4.2 → agent_learning-0.5.0}/pyproject.toml +1 -1
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/_version.py +1 -1
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/cli.py +264 -6
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/base.py +3 -1
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/cosmos.py +8 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/local.py +4 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/memory.py +4 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0/src/agent_learning.egg-info}/PKG-INFO +13 -5
- agent_learning-0.5.0/tests/test_cli.py +648 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_storage_local.py +3 -0
- agent_learning-0.4.2/PYPI.md +0 -15
- agent_learning-0.4.2/tests/test_cli.py +0 -320
- {agent_learning-0.4.2 → agent_learning-0.5.0}/LICENSE +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/setup.cfg +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/capture.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/router.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/_base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/adherence.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/completion.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/classifiers/scorers/intent.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/config.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/learners/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/learners/base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/learners/reinforce.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/intent_resolution.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/local.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/registry.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/task_adherence.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/metrics/task_completion.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/policy/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/policy/base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/policy/contextual_softmax.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/policy/softmax_bandit.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/py.typed +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/rewards/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/rewards/shaping.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/rewards/writer.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/_base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/adherence.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/completion.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/llm/intent.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/_base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/adherence.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/completion.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp/intent.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/_base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/adherence.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/completion.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/nlp_text/intent.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/_base.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/adherence.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/completion.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/slm/intent.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/_text.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/adherence.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/completion.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/scorers/stdlib/intent.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/storage/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/training/__init__.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/training/runner.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning/types.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/SOURCES.txt +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/dependency_links.txt +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/entry_points.txt +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/requires.txt +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/src/agent_learning.egg-info/top_level.txt +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_capture.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_contextual_policy.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_default_store.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_end_to_end.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_learner.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_metrics_local.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_policy.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers_llm.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers_nlp_text.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers_slm.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_scorers_stdlib.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_shaping.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_storage_memory.py +0 -0
- {agent_learning-0.4.2 → agent_learning-0.5.0}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-learning
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Native, in-process reinforcement learning SDK for AI agents — in-memory and local-file storage by default.
|
|
5
5
|
Author: Chris Tava
|
|
6
6
|
License: MIT License
|
|
@@ -67,16 +67,24 @@ Dynamic: license-file
|
|
|
67
67
|
|
|
68
68
|
# agent-learning
|
|
69
69
|
|
|
70
|
-
Native reinforcement learning SDK for AI agents. An in-process
|
|
70
|
+
Native reinforcement learning SDK for AI agents. An in-process Learner optimizes a small, interpretable TaskPolicy over discrete agent choices (understand intent and complete task by choosing the right outcome).
|
|
71
|
+
|
|
72
|
+
TaskPolicies model reusable decisions among executable alternatives such as
|
|
73
|
+
models, skills, tools, workflows, or workloads. Factual questions, ordinary
|
|
74
|
+
chat, reporting, and learning automation are not policy tasks.
|
|
71
75
|
|
|
72
76
|
## How it works
|
|
73
77
|
|
|
74
78
|
The SDK improves agents without LLM weight fine-tuning. There are no GPU fine-tune jobs and no opaque update cycles — just three pieces that run in your existing Python process:
|
|
75
79
|
|
|
76
|
-
1.
|
|
80
|
+
1. **TaskPolicy** is a softmax distribution over `N` discrete actions (e.g., "take action A", "take action B", "take action C"). It lives in Python and updates in milliseconds.
|
|
81
|
+
|
|
82
|
+
2. **Score** evaluates each episode on-device with three stdlib scorers for intent resolution, task adherence, and task completion. Their scores are combined into a single scalar reward with no scoring endpoint or environment variables required. Azure AI evaluators remain available as an opt-in.
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
3. **Learner** applies REINFORCE-with-baseline to update TaskPolicy logits directly from logged episodes. Updates are tiny gradient steps that run on local compute and persist through a pluggable store — in-memory or local files by default, with Azure Cosmos DB optional.
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
`task-policy-decide` closes the loop at execution time by returning the selected
|
|
87
|
+
action plus historical correctness, reward, result summaries, and per-metric
|
|
88
|
+
quality feedback for the agent to use on its next delegated decision.
|
|
81
89
|
|
|
82
90
|
Every episode, reward, run, and deployment is captured by the configured store — in-memory or local files by default, or Azure Cosmos DB — giving you a complete lineage and audit trail of how the policy evolved over time.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# agent-learning
|
|
2
|
+
|
|
3
|
+
Native reinforcement learning SDK for AI agents. An in-process Learner optimizes a small, interpretable TaskPolicy over discrete agent choices (understand intent and complete task by choosing the right outcome).
|
|
4
|
+
|
|
5
|
+
TaskPolicies model reusable decisions among executable alternatives such as
|
|
6
|
+
models, skills, tools, workflows, or workloads. Factual questions, ordinary
|
|
7
|
+
chat, reporting, and learning automation are not policy tasks.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
The SDK improves agents without LLM weight fine-tuning. There are no GPU fine-tune jobs and no opaque update cycles — just three pieces that run in your existing Python process:
|
|
12
|
+
|
|
13
|
+
1. **TaskPolicy** is a softmax distribution over `N` discrete actions (e.g., "take action A", "take action B", "take action C"). It lives in Python and updates in milliseconds.
|
|
14
|
+
|
|
15
|
+
2. **Score** evaluates each episode on-device with three stdlib scorers for intent resolution, task adherence, and task completion. Their scores are combined into a single scalar reward with no scoring endpoint or environment variables required. Azure AI evaluators remain available as an opt-in.
|
|
16
|
+
|
|
17
|
+
3. **Learner** applies REINFORCE-with-baseline to update TaskPolicy logits directly from logged episodes. Updates are tiny gradient steps that run on local compute and persist through a pluggable store — in-memory or local files by default, with Azure Cosmos DB optional.
|
|
18
|
+
|
|
19
|
+
`task-policy-decide` closes the loop at execution time by returning the selected
|
|
20
|
+
action plus historical correctness, reward, result summaries, and per-metric
|
|
21
|
+
quality feedback for the agent to use on its next delegated decision.
|
|
22
|
+
|
|
23
|
+
Every episode, reward, run, and deployment is captured by the configured store — in-memory or local files by default, or Azure Cosmos DB — giving you a complete lineage and audit trail of how the policy evolved over time.
|
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
# agent-learning
|
|
2
2
|
|
|
3
3
|
Native reinforcement learning SDK for AI agents. An in-process
|
|
4
|
-
|
|
4
|
+
Learner optimizes a small, interpretable TaskPolicy over discrete agent choices (e.g., "take action A", "take action B", "take action C") using on-device evaluation scores as the reward
|
|
5
5
|
signal by default.
|
|
6
6
|
|
|
7
|
+
TaskPolicies represent reusable **decisions among executable alternatives**.
|
|
8
|
+
They are not conversation logs: factual questions, ordinary chat, reporting,
|
|
9
|
+
and agent-learning automation are not policy tasks.
|
|
10
|
+
|
|
7
11
|
<p align="center">
|
|
8
|
-
<img src="images/agent-learning-loop.svg" alt="Animated loop:
|
|
12
|
+
<img src="images/agent-learning-loop.svg" alt="Animated TaskPolicy Score Learner loop: TaskPolicy chooses a task action, Score evaluates the episode, and Learner updates TaskPolicy" width="960" style="max-width:100%; height:auto;" />
|
|
9
13
|
</p>
|
|
10
14
|
|
|
11
15
|
## How it works
|
|
12
16
|
|
|
13
17
|
The SDK improves agents without LLM weight fine-tuning. There are no GPU fine-tune jobs and no opaque update cycles — just three pieces that run in your existing Python process:
|
|
14
18
|
|
|
15
|
-
1.
|
|
19
|
+
1. **TaskPolicy** is a softmax distribution over `N` discrete
|
|
16
20
|
actions (e.g., "take action A", "take action B", "take action C"). It lives in Python and updates in milliseconds.
|
|
17
21
|
|
|
18
|
-
<img src="images/0f85e08d0c47cd01.png" alt="
|
|
22
|
+
<img src="images/0f85e08d0c47cd01.png" alt="TaskPolicy selects one of N discrete actions" width="360" style="max-width:100%; height:auto;" />
|
|
19
23
|
|
|
20
|
-
2.
|
|
24
|
+
2. **Score** evaluates each episode locally with three stdlib scorers for intent
|
|
21
25
|
resolution, task adherence, and task completion. Their scores are combined
|
|
22
26
|
into one scalar reward. No scoring endpoint or environment variable is
|
|
23
27
|
required. Configured Azure AI evaluators remain available as an opt-in.
|
|
24
28
|
|
|
25
29
|
<img src="images/246d112f995b785a.png" alt="Three evaluator scores feed a single scalar reward" width="360" style="max-width:100%; height:auto;" />
|
|
26
30
|
|
|
27
|
-
3.
|
|
31
|
+
3. **Learner** applies REINFORCE-with-baseline to update TaskPolicy logits
|
|
28
32
|
directly from stored episodes. Updates are tiny gradient steps
|
|
29
33
|
that run on local compute and persist through a pluggable store — in-memory
|
|
30
34
|
or local files by default, with Azure Cosmos DB optional.
|
|
31
35
|
|
|
32
36
|
<img src="images/cc970c453583c982.png" alt="Policy quality improves with every batch of episodes" width="360" style="max-width:100%; height:auto;" />
|
|
33
37
|
|
|
38
|
+
Before the next delegated execution, `task-policy-decide` samples the learned
|
|
39
|
+
policy and returns the selected action together with correctness rate, mean
|
|
40
|
+
reward, recent result summaries, and intent/adherence/completion scores. Agents
|
|
41
|
+
consume that feedback rather than training a policy that is never used.
|
|
42
|
+
|
|
34
43
|
Every episode, reward, run, and deployment is captured by the
|
|
35
44
|
configured store — in-memory or local files by default, or Azure Cosmos DB —
|
|
36
45
|
giving you a complete lineage and audit trail of how the policy
|
|
@@ -71,12 +80,14 @@ The `agent-learn` CLI provides the current task-learning-loop operations:
|
|
|
71
80
|
|
|
72
81
|
```text
|
|
73
82
|
agent-learn list
|
|
74
|
-
agent-learn
|
|
75
|
-
agent-learn
|
|
76
|
-
agent-learn task-episodes-
|
|
77
|
-
agent-learn task-
|
|
78
|
-
agent-learn task-
|
|
83
|
+
agent-learn --version
|
|
84
|
+
agent-learn tasks-list <agent_id> [--decision-only]
|
|
85
|
+
agent-learn task-episodes-count <agent_id> [--task-id <task_id>] [--start-date <date>] [--end-date <date>]
|
|
86
|
+
agent-learn task-episodes-list <agent_id> [--task-id <task_id>] [--limit <1-500>] [--include-incomplete] [--start-date <date>] [--end-date <date>]
|
|
87
|
+
agent-learn task-policy-init --agent-id <agent_id> --task-id <task_id> --decision-context <context> --actions ./actions.json
|
|
88
|
+
agent-learn task-policy-decide --agent-id <agent_id> --task-id <task_id> [--history-limit <1-500>] [--greedy] [--seed <integer>]
|
|
89
|
+
agent-learn task-episode-register --agent-id <agent_id> --task-id <task_id> --episode ./episode.json [--require-decision-policy]
|
|
79
90
|
agent-learn score --agent-id <agent_id> [--task-id <task_id>] [--limit <1-500>]
|
|
80
|
-
agent-learn train --agent-id <agent_id> [--task-id <task_id>] [--limit <1-500>] [--start-date <date>] [--end-date <date>] [--skip-scoring]
|
|
91
|
+
agent-learn train --agent-id <agent_id> [--task-id <task_id>] [--decision-only] [--limit <1-500>] [--min-episodes <1-500>] [--start-date <date>] [--end-date <date>] [--skip-scoring]
|
|
81
92
|
agent-learn task-policy --agent-id <agent_id> --task-id <task_id>
|
|
82
93
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-learning"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Native, in-process reinforcement learning SDK for AI agents — in-memory and local-file storage by default."
|
|
9
9
|
readme = "PYPI.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -5,11 +5,15 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
import math
|
|
9
|
+
import random
|
|
8
10
|
import sys
|
|
9
11
|
import uuid
|
|
10
12
|
from collections import Counter
|
|
13
|
+
from datetime import datetime, timezone
|
|
11
14
|
from typing import Any
|
|
12
15
|
|
|
16
|
+
from ._version import __version__
|
|
13
17
|
from .policy.softmax_bandit import SoftmaxPolicy
|
|
14
18
|
from .storage.cosmos import get_default_store
|
|
15
19
|
from .training.runner import LearningRunner
|
|
@@ -18,6 +22,7 @@ from .types import Action, Episode, MetricName, PolicySnapshot, RewardSource
|
|
|
18
22
|
logger = logging.getLogger(__name__)
|
|
19
23
|
|
|
20
24
|
_MAX_EPISODES = 500
|
|
25
|
+
_DECISION_POLICY_SCOPE = "delegated_decision"
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
def _episode_limit(value: str) -> int:
|
|
@@ -27,14 +32,29 @@ def _episode_limit(value: str) -> int:
|
|
|
27
32
|
return limit
|
|
28
33
|
|
|
29
34
|
|
|
35
|
+
def _iso_date(value: str) -> str:
|
|
36
|
+
try:
|
|
37
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
raise argparse.ArgumentTypeError(f"invalid ISO 8601 date: {value!r}") from exc
|
|
40
|
+
if parsed.tzinfo is None:
|
|
41
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
42
|
+
return parsed.astimezone(timezone.utc).isoformat()
|
|
43
|
+
|
|
44
|
+
|
|
30
45
|
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
31
|
-
parser = argparse.ArgumentParser(
|
|
46
|
+
parser = argparse.ArgumentParser(
|
|
47
|
+
prog="agent-learn",
|
|
48
|
+
description=f"Native RL CLI for AI agents. SDK version {__version__}.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
32
51
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
33
52
|
|
|
34
53
|
sub.add_parser("list", help="List discovered agent ids and names.")
|
|
35
54
|
|
|
36
55
|
tasks = sub.add_parser("tasks-list", help="List tasks for an agent.")
|
|
37
56
|
tasks.add_argument("agent_id")
|
|
57
|
+
tasks.add_argument("--decision-only", action="store_true")
|
|
38
58
|
|
|
39
59
|
count = sub.add_parser(
|
|
40
60
|
"task-episodes-count",
|
|
@@ -42,6 +62,8 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|
|
42
62
|
)
|
|
43
63
|
count.add_argument("agent_id")
|
|
44
64
|
count.add_argument("--task-id")
|
|
65
|
+
count.add_argument("--start-date", type=_iso_date)
|
|
66
|
+
count.add_argument("--end-date", type=_iso_date)
|
|
45
67
|
|
|
46
68
|
episodes = sub.add_parser(
|
|
47
69
|
"task-episodes-list",
|
|
@@ -51,13 +73,17 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|
|
51
73
|
episodes.add_argument("--task-id")
|
|
52
74
|
episodes.add_argument("--limit", type=_episode_limit, default=_MAX_EPISODES)
|
|
53
75
|
episodes.add_argument("--include-incomplete", action="store_true")
|
|
76
|
+
episodes.add_argument("--start-date", type=_iso_date)
|
|
77
|
+
episodes.add_argument("--end-date", type=_iso_date)
|
|
54
78
|
|
|
55
79
|
train = sub.add_parser("train", help="Run one offline learning batch.")
|
|
56
80
|
train.add_argument("--agent-id", required=True)
|
|
57
81
|
train.add_argument("--task-id")
|
|
58
82
|
train.add_argument("--limit", type=_episode_limit, default=200)
|
|
59
|
-
train.add_argument("--
|
|
60
|
-
train.add_argument("--
|
|
83
|
+
train.add_argument("--min-episodes", type=_episode_limit, default=1)
|
|
84
|
+
train.add_argument("--decision-only", action="store_true")
|
|
85
|
+
train.add_argument("--start-date", type=_iso_date)
|
|
86
|
+
train.add_argument("--end-date", type=_iso_date)
|
|
61
87
|
train.add_argument(
|
|
62
88
|
"--skip-scoring",
|
|
63
89
|
action="store_true",
|
|
@@ -73,12 +99,27 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|
|
73
99
|
show.add_argument("--agent-id", required=True)
|
|
74
100
|
show.add_argument("--task-id", required=True)
|
|
75
101
|
|
|
102
|
+
decide = sub.add_parser(
|
|
103
|
+
"task-policy-decide",
|
|
104
|
+
help="Choose a delegated decision action and return learned feedback.",
|
|
105
|
+
)
|
|
106
|
+
decide.add_argument("--agent-id", required=True)
|
|
107
|
+
decide.add_argument("--task-id", required=True)
|
|
108
|
+
decide.add_argument("--history-limit", type=_episode_limit, default=100)
|
|
109
|
+
decide.add_argument("--greedy", action="store_true")
|
|
110
|
+
decide.add_argument("--seed", type=int)
|
|
111
|
+
|
|
76
112
|
init = sub.add_parser(
|
|
77
113
|
"task-policy-init",
|
|
78
114
|
help="Create and activate the initial policy for an agent task.",
|
|
79
115
|
)
|
|
80
116
|
init.add_argument("--agent-id", required=True)
|
|
81
117
|
init.add_argument("--task-id", required=True)
|
|
118
|
+
init.add_argument(
|
|
119
|
+
"--decision-context",
|
|
120
|
+
required=True,
|
|
121
|
+
help="Stable description of the delegated choice this policy controls.",
|
|
122
|
+
)
|
|
82
123
|
init.add_argument(
|
|
83
124
|
"--actions",
|
|
84
125
|
required=True,
|
|
@@ -91,6 +132,7 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|
|
91
132
|
)
|
|
92
133
|
register.add_argument("--agent-id", required=True)
|
|
93
134
|
register.add_argument("--task-id", required=True)
|
|
135
|
+
register.add_argument("--require-decision-policy", action="store_true")
|
|
94
136
|
register.add_argument(
|
|
95
137
|
"--episode",
|
|
96
138
|
required=True,
|
|
@@ -108,7 +150,14 @@ def _cmd_agents_list(args: argparse.Namespace) -> int:
|
|
|
108
150
|
|
|
109
151
|
|
|
110
152
|
def _cmd_agent_tasks_list(args: argparse.Namespace) -> int:
|
|
111
|
-
|
|
153
|
+
store = get_default_store()
|
|
154
|
+
tasks = store.list_agent_tasks(args.agent_id)
|
|
155
|
+
if args.decision_only:
|
|
156
|
+
tasks = [
|
|
157
|
+
task
|
|
158
|
+
for task in tasks
|
|
159
|
+
if _is_decision_policy(store.get_active_policy(args.agent_id, task.id))
|
|
160
|
+
]
|
|
112
161
|
print(json.dumps([{"id": task.id, "name": task.name} for task in tasks], indent=2))
|
|
113
162
|
return 0
|
|
114
163
|
|
|
@@ -118,6 +167,8 @@ def _cmd_agents_episodes_count(args: argparse.Namespace) -> int:
|
|
|
118
167
|
args.agent_id,
|
|
119
168
|
task_id=args.task_id,
|
|
120
169
|
full_only=True,
|
|
170
|
+
start_date=args.start_date,
|
|
171
|
+
end_date=args.end_date,
|
|
121
172
|
)
|
|
122
173
|
print(count)
|
|
123
174
|
return 0
|
|
@@ -129,6 +180,8 @@ def _cmd_agents_episodes_list(args: argparse.Namespace) -> int:
|
|
|
129
180
|
args.agent_id,
|
|
130
181
|
task_id=args.task_id,
|
|
131
182
|
limit=_MAX_EPISODES,
|
|
183
|
+
start_date=args.start_date,
|
|
184
|
+
end_date=args.end_date,
|
|
132
185
|
)
|
|
133
186
|
if not args.include_incomplete:
|
|
134
187
|
episodes = [episode for episode in episodes if episode.is_full]
|
|
@@ -179,10 +232,24 @@ def _cmd_train(args: argparse.Namespace) -> int:
|
|
|
179
232
|
if snapshot is None:
|
|
180
233
|
skipped.append({"task_id": task_id, "reason": "no active policy"})
|
|
181
234
|
continue
|
|
235
|
+
if args.decision_only and not _is_decision_policy(snapshot):
|
|
236
|
+
skipped.append({"task_id": task_id, "reason": "not a delegated decision policy"})
|
|
237
|
+
continue
|
|
182
238
|
episode_limit = episode_limits.get(task_id, 0)
|
|
183
239
|
if episode_limit == 0:
|
|
184
240
|
skipped.append({"task_id": task_id, "reason": "no episodes in selected batch"})
|
|
185
241
|
continue
|
|
242
|
+
if episode_limit < args.min_episodes:
|
|
243
|
+
skipped.append(
|
|
244
|
+
{
|
|
245
|
+
"task_id": task_id,
|
|
246
|
+
"reason": (
|
|
247
|
+
f"selected batch has {episode_limit} episodes; "
|
|
248
|
+
f"minimum is {args.min_episodes}"
|
|
249
|
+
),
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
continue
|
|
186
253
|
policy = SoftmaxPolicy.from_snapshot(snapshot)
|
|
187
254
|
runner = LearningRunner(store=store, policy=policy)
|
|
188
255
|
run = runner.run_offline_batch(
|
|
@@ -235,6 +302,89 @@ def _policy_payload(snapshot: PolicySnapshot) -> dict[str, Any]:
|
|
|
235
302
|
return payload
|
|
236
303
|
|
|
237
304
|
|
|
305
|
+
def _is_decision_policy(snapshot: PolicySnapshot | None) -> bool:
|
|
306
|
+
return bool(
|
|
307
|
+
snapshot
|
|
308
|
+
and snapshot.metadata.get("policy_scope") == _DECISION_POLICY_SCOPE
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _latest_aggregate(store: Any, episode: Episode) -> float | None:
|
|
313
|
+
rewards = [
|
|
314
|
+
reward
|
|
315
|
+
for reward in store.get_rewards_for_episode(episode.id, episode.agent_id)
|
|
316
|
+
if reward.source == RewardSource.AGGREGATE
|
|
317
|
+
]
|
|
318
|
+
if not rewards:
|
|
319
|
+
return None
|
|
320
|
+
return max(rewards, key=lambda reward: reward.created_at).value
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _decision_feedback(
|
|
324
|
+
store: Any, snapshot: PolicySnapshot, history_limit: int
|
|
325
|
+
) -> dict[str, Any]:
|
|
326
|
+
stats = {
|
|
327
|
+
action.id: {
|
|
328
|
+
"attempts": 0,
|
|
329
|
+
"correctness_evaluated": 0,
|
|
330
|
+
"correct": 0,
|
|
331
|
+
"correctness_rate": None,
|
|
332
|
+
"rewarded_episodes": 0,
|
|
333
|
+
"mean_reward": None,
|
|
334
|
+
"recent_outcomes": [],
|
|
335
|
+
}
|
|
336
|
+
for action in snapshot.actions
|
|
337
|
+
}
|
|
338
|
+
reward_totals = {action.id: 0.0 for action in snapshot.actions}
|
|
339
|
+
episodes = store.query_episodes(
|
|
340
|
+
snapshot.agent_id,
|
|
341
|
+
task_id=snapshot.task_id,
|
|
342
|
+
limit=history_limit,
|
|
343
|
+
)
|
|
344
|
+
for episode in episodes:
|
|
345
|
+
action_id = episode.action_id
|
|
346
|
+
if action_id not in stats:
|
|
347
|
+
continue
|
|
348
|
+
action_stats = stats[action_id]
|
|
349
|
+
action_stats["attempts"] += 1
|
|
350
|
+
correct_action_id = episode.metadata.get("correct_action_id")
|
|
351
|
+
was_correct = None
|
|
352
|
+
if correct_action_id:
|
|
353
|
+
was_correct = action_id == correct_action_id
|
|
354
|
+
action_stats["correctness_evaluated"] += 1
|
|
355
|
+
action_stats["correct"] += int(was_correct)
|
|
356
|
+
reward = _latest_aggregate(store, episode)
|
|
357
|
+
if reward is not None:
|
|
358
|
+
action_stats["rewarded_episodes"] += 1
|
|
359
|
+
reward_totals[action_id] += reward
|
|
360
|
+
if len(action_stats["recent_outcomes"]) < 3:
|
|
361
|
+
score_breakdown = {}
|
|
362
|
+
for result in store.get_metric_results(episode.id, episode.agent_id):
|
|
363
|
+
score_breakdown[result.metric.value] = {
|
|
364
|
+
"normalized": result.normalized,
|
|
365
|
+
"status": result.status,
|
|
366
|
+
"reason": result.reason,
|
|
367
|
+
}
|
|
368
|
+
action_stats["recent_outcomes"].append(
|
|
369
|
+
{
|
|
370
|
+
"created_at": episode.created_at,
|
|
371
|
+
"was_correct": was_correct,
|
|
372
|
+
"reward": reward,
|
|
373
|
+
"execution_status": episode.execution_status,
|
|
374
|
+
"result_summary": episode.result_summary,
|
|
375
|
+
"score_breakdown": score_breakdown,
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
for action_id, action_stats in stats.items():
|
|
379
|
+
evaluated = action_stats["correctness_evaluated"]
|
|
380
|
+
rewarded = action_stats["rewarded_episodes"]
|
|
381
|
+
if evaluated:
|
|
382
|
+
action_stats["correctness_rate"] = action_stats["correct"] / evaluated
|
|
383
|
+
if rewarded:
|
|
384
|
+
action_stats["mean_reward"] = reward_totals[action_id] / rewarded
|
|
385
|
+
return {"episodes_reviewed": len(episodes), "actions": stats}
|
|
386
|
+
|
|
387
|
+
|
|
238
388
|
def _policy_difference(
|
|
239
389
|
current: PolicySnapshot, previous: PolicySnapshot | None
|
|
240
390
|
) -> dict[str, Any] | None:
|
|
@@ -295,6 +445,78 @@ def _cmd_show_task_policy(args: argparse.Namespace) -> int:
|
|
|
295
445
|
return 0
|
|
296
446
|
|
|
297
447
|
|
|
448
|
+
def _cmd_decide_task_policy(args: argparse.Namespace) -> int:
|
|
449
|
+
store = get_default_store()
|
|
450
|
+
snapshot = store.get_active_policy(args.agent_id, args.task_id)
|
|
451
|
+
if snapshot is None:
|
|
452
|
+
print(
|
|
453
|
+
f"No active policy found for agent_id={args.agent_id!r}, "
|
|
454
|
+
f"task_id={args.task_id!r}.",
|
|
455
|
+
file=sys.stderr,
|
|
456
|
+
)
|
|
457
|
+
return 2
|
|
458
|
+
if not _is_decision_policy(snapshot):
|
|
459
|
+
print(
|
|
460
|
+
"The active policy is not marked as a delegated decision policy. "
|
|
461
|
+
"Questions, reporting tasks, and agent-learning automation are not eligible.",
|
|
462
|
+
file=sys.stderr,
|
|
463
|
+
)
|
|
464
|
+
return 2
|
|
465
|
+
rng = random.Random(args.seed) if args.seed is not None else None
|
|
466
|
+
policy = SoftmaxPolicy.from_snapshot(snapshot, rng=rng)
|
|
467
|
+
probabilities = policy.probabilities()
|
|
468
|
+
recommended_index = max(range(len(probabilities)), key=probabilities.__getitem__)
|
|
469
|
+
if args.greedy:
|
|
470
|
+
selected_index = recommended_index
|
|
471
|
+
selected_action = snapshot.actions[selected_index]
|
|
472
|
+
selected_probability = probabilities[selected_index]
|
|
473
|
+
logprob = math.log(max(selected_probability, 1e-12))
|
|
474
|
+
mode = "greedy"
|
|
475
|
+
else:
|
|
476
|
+
decision = policy.choose()
|
|
477
|
+
selected_action = decision.action
|
|
478
|
+
selected_index = next(
|
|
479
|
+
index
|
|
480
|
+
for index, action in enumerate(snapshot.actions)
|
|
481
|
+
if action.id == selected_action.id
|
|
482
|
+
)
|
|
483
|
+
selected_probability = probabilities[selected_index]
|
|
484
|
+
logprob = decision.logprob
|
|
485
|
+
mode = "sampled"
|
|
486
|
+
feedback = _decision_feedback(store, snapshot, args.history_limit)
|
|
487
|
+
selected_stats = feedback["actions"][selected_action.id]
|
|
488
|
+
recommendation = snapshot.actions[recommended_index]
|
|
489
|
+
print(
|
|
490
|
+
json.dumps(
|
|
491
|
+
{
|
|
492
|
+
"agent_id": snapshot.agent_id,
|
|
493
|
+
"task_id": snapshot.task_id,
|
|
494
|
+
"decision_context": snapshot.metadata.get("decision_context"),
|
|
495
|
+
"policy_id": snapshot.id,
|
|
496
|
+
"policy_version": snapshot.version,
|
|
497
|
+
"selection_mode": mode,
|
|
498
|
+
"selected_action": {
|
|
499
|
+
**selected_action.to_dict(),
|
|
500
|
+
"probability": selected_probability,
|
|
501
|
+
"logprob": logprob,
|
|
502
|
+
},
|
|
503
|
+
"recommended_action": {
|
|
504
|
+
**recommendation.to_dict(),
|
|
505
|
+
"probability": probabilities[recommended_index],
|
|
506
|
+
},
|
|
507
|
+
"action_probabilities": {
|
|
508
|
+
action.id: probability
|
|
509
|
+
for action, probability in zip(snapshot.actions, probabilities)
|
|
510
|
+
},
|
|
511
|
+
"selected_action_feedback": selected_stats,
|
|
512
|
+
"historical_feedback": feedback,
|
|
513
|
+
},
|
|
514
|
+
indent=2,
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
return 0
|
|
518
|
+
|
|
519
|
+
|
|
298
520
|
def _cmd_init_task_policy(args: argparse.Namespace) -> int:
|
|
299
521
|
store = get_default_store()
|
|
300
522
|
if store.get_active_policy(args.agent_id, args.task_id) is not None:
|
|
@@ -310,20 +532,35 @@ def _cmd_init_task_policy(args: argparse.Namespace) -> int:
|
|
|
310
532
|
except (OSError, json.JSONDecodeError) as exc:
|
|
311
533
|
print(f"Unable to read --actions file: {exc}", file=sys.stderr)
|
|
312
534
|
return 2
|
|
313
|
-
if not isinstance(action_payloads, list) or
|
|
314
|
-
print(
|
|
535
|
+
if not isinstance(action_payloads, list) or len(action_payloads) < 2:
|
|
536
|
+
print(
|
|
537
|
+
"--actions file must contain at least two delegated decision actions",
|
|
538
|
+
file=sys.stderr,
|
|
539
|
+
)
|
|
315
540
|
return 2
|
|
316
541
|
try:
|
|
317
542
|
actions = [Action.from_dict(item) for item in action_payloads]
|
|
318
543
|
except (KeyError, TypeError, ValueError) as exc:
|
|
319
544
|
print(f"Invalid action definition: {exc}", file=sys.stderr)
|
|
320
545
|
return 2
|
|
546
|
+
action_ids = [action.id for action in actions]
|
|
547
|
+
if any(not action_id.strip() for action_id in action_ids) or len(set(action_ids)) != len(
|
|
548
|
+
action_ids
|
|
549
|
+
):
|
|
550
|
+
print("Decision action ids must be non-empty and unique", file=sys.stderr)
|
|
551
|
+
return 2
|
|
321
552
|
policy = SoftmaxPolicy.from_actions(
|
|
322
553
|
actions,
|
|
323
554
|
agent_id=args.agent_id,
|
|
324
555
|
task_id=args.task_id,
|
|
325
556
|
)
|
|
326
557
|
snapshot = policy.snapshot()
|
|
558
|
+
snapshot.metadata.update(
|
|
559
|
+
{
|
|
560
|
+
"policy_scope": _DECISION_POLICY_SCOPE,
|
|
561
|
+
"decision_context": args.decision_context,
|
|
562
|
+
}
|
|
563
|
+
)
|
|
327
564
|
store.store_policy(snapshot)
|
|
328
565
|
print(json.dumps(_policy_payload(snapshot), indent=2))
|
|
329
566
|
return 0
|
|
@@ -358,6 +595,26 @@ def _cmd_register_task_episode(args: argparse.Namespace) -> int:
|
|
|
358
595
|
print(f"Invalid episode definition: {exc}", file=sys.stderr)
|
|
359
596
|
return 2
|
|
360
597
|
|
|
598
|
+
if args.require_decision_policy:
|
|
599
|
+
policy = get_default_store().get_policy(episode.policy_id or "", args.agent_id)
|
|
600
|
+
if not _is_decision_policy(policy) or policy.task_id != args.task_id:
|
|
601
|
+
print(
|
|
602
|
+
"Episode registration requires a delegated decision policy and its policy_id.",
|
|
603
|
+
file=sys.stderr,
|
|
604
|
+
)
|
|
605
|
+
return 2
|
|
606
|
+
action_ids = {action.id for action in policy.actions}
|
|
607
|
+
if episode.action_id not in action_ids:
|
|
608
|
+
print("Episode action_id is not in the delegated decision policy.", file=sys.stderr)
|
|
609
|
+
return 2
|
|
610
|
+
correct_action_id = episode.metadata.get("correct_action_id")
|
|
611
|
+
if correct_action_id is not None and correct_action_id not in action_ids:
|
|
612
|
+
print(
|
|
613
|
+
"Episode metadata.correct_action_id is not in the delegated decision policy.",
|
|
614
|
+
file=sys.stderr,
|
|
615
|
+
)
|
|
616
|
+
return 2
|
|
617
|
+
|
|
361
618
|
get_default_store().store_episode(episode)
|
|
362
619
|
print(json.dumps(episode.to_dict(), indent=2))
|
|
363
620
|
return 0
|
|
@@ -375,6 +632,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
375
632
|
"train": _cmd_train,
|
|
376
633
|
"score": _cmd_score,
|
|
377
634
|
"task-policy": _cmd_show_task_policy,
|
|
635
|
+
"task-policy-decide": _cmd_decide_task_policy,
|
|
378
636
|
"task-policy-init": _cmd_init_task_policy,
|
|
379
637
|
"task-episode-register": _cmd_register_task_episode,
|
|
380
638
|
}
|
|
@@ -66,8 +66,10 @@ class LearningStore(ABC):
|
|
|
66
66
|
*,
|
|
67
67
|
task_id: Optional[str] = None,
|
|
68
68
|
full_only: bool = False,
|
|
69
|
+
start_date: Optional[str] = None,
|
|
70
|
+
end_date: Optional[str] = None,
|
|
69
71
|
) -> int:
|
|
70
|
-
"""Count episodes
|
|
72
|
+
"""Count episodes filtered by task, completeness, or time window."""
|
|
71
73
|
|
|
72
74
|
# ---- Metric results -------------------------------------------
|
|
73
75
|
|
|
@@ -272,6 +272,8 @@ class CosmosStore(LearningStore):
|
|
|
272
272
|
*,
|
|
273
273
|
task_id: Optional[str] = None,
|
|
274
274
|
full_only: bool = False,
|
|
275
|
+
start_date: Optional[str] = None,
|
|
276
|
+
end_date: Optional[str] = None,
|
|
275
277
|
) -> int:
|
|
276
278
|
clauses = ["c.agent_id = @agent_id"]
|
|
277
279
|
params: List[Dict[str, Any]] = [{"name": "@agent_id", "value": agent_id}]
|
|
@@ -281,6 +283,12 @@ class CosmosStore(LearningStore):
|
|
|
281
283
|
else:
|
|
282
284
|
clauses.append("c.task_id = @task_id")
|
|
283
285
|
params.append({"name": "@task_id", "value": task_id})
|
|
286
|
+
if start_date:
|
|
287
|
+
clauses.append("c.created_at >= @start_date")
|
|
288
|
+
params.append({"name": "@start_date", "value": start_date})
|
|
289
|
+
if end_date:
|
|
290
|
+
clauses.append("c.created_at <= @end_date")
|
|
291
|
+
params.append({"name": "@end_date", "value": end_date})
|
|
284
292
|
if full_only:
|
|
285
293
|
clauses.extend(
|
|
286
294
|
[
|
|
@@ -212,12 +212,16 @@ class LocalFileStore(LearningStore):
|
|
|
212
212
|
*,
|
|
213
213
|
task_id: Optional[str] = None,
|
|
214
214
|
full_only: bool = False,
|
|
215
|
+
start_date: Optional[str] = None,
|
|
216
|
+
end_date: Optional[str] = None,
|
|
215
217
|
) -> int:
|
|
216
218
|
return sum(
|
|
217
219
|
1
|
|
218
220
|
for doc in self._read_dir_docs("episodes", agent_id)
|
|
219
221
|
if (task_id is None or doc.get("task_id", "default") == task_id)
|
|
220
222
|
and (not full_only or Episode.from_dict(doc).is_full)
|
|
223
|
+
and (start_date is None or doc.get("created_at", "") >= start_date)
|
|
224
|
+
and (end_date is None or doc.get("created_at", "") <= end_date)
|
|
221
225
|
)
|
|
222
226
|
|
|
223
227
|
# ------------------------------------------------------------------
|
|
@@ -93,6 +93,8 @@ class InMemoryStore(LearningStore):
|
|
|
93
93
|
*,
|
|
94
94
|
task_id: Optional[str] = None,
|
|
95
95
|
full_only: bool = False,
|
|
96
|
+
start_date: Optional[str] = None,
|
|
97
|
+
end_date: Optional[str] = None,
|
|
96
98
|
) -> int:
|
|
97
99
|
return sum(
|
|
98
100
|
1
|
|
@@ -100,6 +102,8 @@ class InMemoryStore(LearningStore):
|
|
|
100
102
|
if episode.agent_id == agent_id
|
|
101
103
|
and (task_id is None or episode.task_id == task_id)
|
|
102
104
|
and (not full_only or episode.is_full)
|
|
105
|
+
and (start_date is None or episode.created_at >= start_date)
|
|
106
|
+
and (end_date is None or episode.created_at <= end_date)
|
|
103
107
|
)
|
|
104
108
|
|
|
105
109
|
# ---- Metric results -------------------------------------------
|