mojentic 0.6.2__py3-none-any.whl → 0.7.2__py3-none-any.whl
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.
- _examples/async_dispatcher_example.py +241 -0
- _examples/async_llm_example.py +236 -0
- _examples/broker_as_tool.py +13 -10
- _examples/coding_file_tool.py +170 -77
- _examples/file_tool.py +5 -3
- mojentic/__init__.py +2 -7
- mojentic/agents/__init__.py +11 -2
- mojentic/agents/async_aggregator_agent.py +162 -0
- mojentic/agents/async_aggregator_agent_spec.py +227 -0
- mojentic/agents/async_llm_agent.py +197 -0
- mojentic/agents/async_llm_agent_spec.py +166 -0
- mojentic/agents/base_async_agent.py +27 -0
- mojentic/async_dispatcher.py +134 -0
- mojentic/async_dispatcher_spec.py +244 -0
- mojentic/context/__init__.py +4 -0
- mojentic/llm/__init__.py +14 -2
- mojentic/llm/gateways/__init__.py +22 -0
- mojentic/llm/gateways/models.py +3 -3
- mojentic/llm/gateways/ollama.py +4 -4
- mojentic/llm/gateways/openai.py +3 -3
- mojentic/llm/gateways/openai_messages_adapter.py +8 -4
- mojentic/llm/llm_broker.py +4 -4
- mojentic/llm/message_composers.py +1 -1
- mojentic/llm/registry/__init__.py +6 -0
- mojentic/llm/registry/populate_registry_from_ollama.py +13 -12
- mojentic/llm/tools/__init__.py +18 -0
- mojentic/llm/tools/date_resolver.py +5 -2
- mojentic/llm/tools/ephemeral_task_manager/__init__.py +8 -8
- mojentic/llm/tools/file_manager.py +603 -42
- mojentic/llm/tools/file_manager_spec.py +723 -0
- mojentic/llm/tools/tool_wrapper.py +7 -3
- mojentic/tracer/__init__.py +8 -3
- {mojentic-0.6.2.dist-info → mojentic-0.7.2.dist-info}/METADATA +4 -2
- {mojentic-0.6.2.dist-info → mojentic-0.7.2.dist-info}/RECORD +37 -27
- {mojentic-0.6.2.dist-info → mojentic-0.7.2.dist-info}/WHEEL +0 -0
- {mojentic-0.6.2.dist-info → mojentic-0.7.2.dist-info}/licenses/LICENSE.md +0 -0
- {mojentic-0.6.2.dist-info → mojentic-0.7.2.dist-info}/top_level.txt +0 -0
_examples/coding_file_tool.py
CHANGED
|
@@ -1,88 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This example demonstrates the use of all the file management tools available in mojentic.
|
|
3
|
+
|
|
4
|
+
It creates an IterativeProblemSolver with access to a comprehensive set of file management tools:
|
|
5
|
+
- ListFilesTool: List files in the top-level directory
|
|
6
|
+
- ListAllFilesTool: List all files recursively
|
|
7
|
+
- ReadFileTool: Read the content of a file
|
|
8
|
+
- WriteFileTool: Write content to a file
|
|
9
|
+
- CreateDirectoryTool: Create a new directory
|
|
10
|
+
- FindFilesByGlobTool: Find files matching a glob pattern
|
|
11
|
+
- FindFilesContainingTool: Find files containing text matching a regex pattern
|
|
12
|
+
- FindLinesMatchingTool: Find lines in a file matching a regex pattern
|
|
13
|
+
- EditFileWithDiffTool: Edit a file by applying a diff
|
|
14
|
+
|
|
15
|
+
The solver is then given a task that requires using all of these tools.
|
|
16
|
+
"""
|
|
17
|
+
|
|
1
18
|
import os
|
|
2
19
|
from pathlib import Path
|
|
3
20
|
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
from mojentic.agents.base_llm_agent import BaseLLMAgent
|
|
7
|
-
from mojentic.agents.output_agent import OutputAgent
|
|
8
|
-
from mojentic.dispatcher import Dispatcher
|
|
9
|
-
from mojentic.event import Event
|
|
21
|
+
from mojentic.agents.iterative_problem_solver import IterativeProblemSolver
|
|
10
22
|
from mojentic.llm.gateways import OpenAIGateway
|
|
11
23
|
from mojentic.llm.llm_broker import LLMBroker
|
|
12
|
-
from mojentic.llm.tools.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ResponseEvent(Event):
|
|
21
|
-
text: str
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ResponseModel(BaseModel):
|
|
25
|
-
text: str
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
base_dir = Path(__file__).parent.parent.parent.parent / "code-playground"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class RequestAgent(BaseLLMAgent):
|
|
32
|
-
def __init__(self, llm: LLMBroker):
|
|
33
|
-
super().__init__(llm,
|
|
34
|
-
"You are a helpful assistant.")
|
|
35
|
-
self.add_tool(ReadFileTool(str(base_dir)))
|
|
36
|
-
self.add_tool(WriteFileTool(str(base_dir)))
|
|
37
|
-
|
|
38
|
-
def receive_event(self, event):
|
|
39
|
-
response = self.generate_response(event.text)
|
|
40
|
-
return [ResponseEvent(source=type(self), correlation_id=event.correlation_id, text=response)]
|
|
24
|
+
from mojentic.llm.tools.ephemeral_task_manager import EphemeralTaskList, AppendTaskTool, \
|
|
25
|
+
ClearTasksTool, CompleteTaskTool, InsertTaskAfterTool, ListTasksTool, PrependTaskTool, \
|
|
26
|
+
StartTaskTool
|
|
27
|
+
from mojentic.llm.tools.file_manager import (
|
|
28
|
+
ReadFileTool, WriteFileTool, ListFilesTool, ListAllFilesTool,
|
|
29
|
+
FindFilesByGlobTool, FindFilesContainingTool, FindLinesMatchingTool,
|
|
30
|
+
EditFileWithDiffTool, CreateDirectoryTool, FilesystemGateway
|
|
31
|
+
)
|
|
41
32
|
|
|
33
|
+
base_dir = Path(__file__).parent.parent.parent.parent / "code-playground2"
|
|
42
34
|
|
|
43
|
-
#
|
|
44
|
-
# file.write("""
|
|
45
|
-
# Primes
|
|
46
|
-
#
|
|
47
|
-
# You are to write some python code that will generate the first 100 prime numbers.
|
|
48
|
-
#
|
|
49
|
-
# Store the code in a file called primes.py
|
|
50
|
-
# """.strip())
|
|
35
|
+
# Initialize the LLM broker
|
|
51
36
|
|
|
52
|
-
#
|
|
53
|
-
# OK this example is fun, it shows trying to make 2 consecutive
|
|
54
|
-
# tool calls. The first tool call reads a file, the second writes a file.
|
|
55
|
-
#
|
|
56
|
-
# Ollama 3.1 70b seems to handle this consistently, 3.3 70b seems flakey, flakier when num_ctx is set to 32768
|
|
57
|
-
# Ollama 3.1 8b seems to handle it about 1/3 the time
|
|
58
|
-
# OpenAI gpt-4o-mini handles it perfectly every single time
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
# llm = LLMBroker("qwen2.5-coder:32b")
|
|
63
|
-
# llm = LLMBroker("llama3.3")
|
|
64
37
|
api_key = os.getenv("OPENAI_API_KEY")
|
|
65
38
|
gateway = OpenAIGateway(api_key)
|
|
66
|
-
llm = LLMBroker(model="
|
|
67
|
-
request_agent = RequestAgent(llm)
|
|
68
|
-
output_agent = OutputAgent()
|
|
69
|
-
|
|
70
|
-
router = Router({
|
|
71
|
-
RequestEvent: [request_agent, output_agent],
|
|
72
|
-
ResponseEvent: [output_agent]
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
dispatcher = Dispatcher(router)
|
|
76
|
-
dispatcher.dispatch(RequestEvent(source=str, text="""
|
|
77
|
-
# Mandelbrodt Set
|
|
39
|
+
llm = LLMBroker(model="o4-mini", gateway=gateway)
|
|
78
40
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
41
|
+
# llm = LLMBroker("qwen2.5-coder:32b")
|
|
42
|
+
# llm = LLMBroker("llama3.3")
|
|
43
|
+
# llm = LLMBroker(model="qwen3-128k:32b")
|
|
44
|
+
|
|
45
|
+
# Create a filesystem gateway for the sandbox
|
|
46
|
+
fs = FilesystemGateway(base_path=str(base_dir))
|
|
47
|
+
|
|
48
|
+
task_manager = EphemeralTaskList()
|
|
49
|
+
|
|
50
|
+
# Create a list of all file management tools
|
|
51
|
+
tools = [
|
|
52
|
+
ReadFileTool(fs),
|
|
53
|
+
WriteFileTool(fs),
|
|
54
|
+
ListFilesTool(fs),
|
|
55
|
+
ListAllFilesTool(fs),
|
|
56
|
+
CreateDirectoryTool(fs),
|
|
57
|
+
FindFilesByGlobTool(fs),
|
|
58
|
+
FindFilesContainingTool(fs),
|
|
59
|
+
FindLinesMatchingTool(fs),
|
|
60
|
+
EditFileWithDiffTool(fs),
|
|
61
|
+
AppendTaskTool(task_manager),
|
|
62
|
+
ClearTasksTool(task_manager),
|
|
63
|
+
CompleteTaskTool(task_manager),
|
|
64
|
+
InsertTaskAfterTool(task_manager),
|
|
65
|
+
ListTasksTool(task_manager),
|
|
66
|
+
PrependTaskTool(task_manager),
|
|
67
|
+
StartTaskTool(task_manager),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Create the iterative problem solver with the tools
|
|
71
|
+
solver = IterativeProblemSolver(
|
|
72
|
+
llm=llm,
|
|
73
|
+
available_tools=tools,
|
|
74
|
+
max_iterations=5,
|
|
75
|
+
system_prompt="""
|
|
76
|
+
# 0 - Project Identity & Context
|
|
77
|
+
|
|
78
|
+
You are an expert and principled software engineer, well versed in writing Python games. You work
|
|
79
|
+
carefully and purposefully and always check your work with an eye to testability and correctness.
|
|
80
|
+
You know that every line of code you write is a liability, and you take care that every line
|
|
81
|
+
matters.
|
|
82
|
+
|
|
83
|
+
# 1 - Universal Engineering Principles
|
|
84
|
+
|
|
85
|
+
* **Code is communication** — optimise for the next human reader.
|
|
86
|
+
* **Simple Design Heuristics** — guiding principles, not iron laws; consult the user when you
|
|
87
|
+
need to break them.
|
|
88
|
+
1. **All tests pass** — correctness is non‑negotiable.
|
|
89
|
+
2. **Reveals intent** — code should read like an explanation.
|
|
90
|
+
3. **No *****knowledge***** duplication** — avoid multiple spots that must change together;
|
|
91
|
+
identical code is only a smell when it hides duplicate *decisions*.
|
|
92
|
+
4. **Minimal entities** — remove unnecessary indirection, classes, or parameters.
|
|
93
|
+
* **Small, safe increments** — single‑reason commits; avoid speculative work (**YAGNI**).
|
|
94
|
+
* **Tests are the executable spec** — red first, green always; test behaviour not implementation.
|
|
95
|
+
* **Compose over inherit**; favour pure functions where practical, avoid side-effects.
|
|
96
|
+
* **Functional core, imperative shell** — isolate pure business logic from I/O and side effects;
|
|
97
|
+
push mutations to the system boundaries, build mockable gateways at those boundaries.
|
|
98
|
+
* **Psychological safety** — review code, not colleagues; critique ideas, not authors.
|
|
99
|
+
* **Version‑control etiquette** — descriptive commit messages, branch from `main`, PRs require
|
|
100
|
+
green CI.
|
|
101
|
+
|
|
102
|
+
# 2 - Python‑Specific Conventions
|
|
103
|
+
|
|
104
|
+
## 2.1 Runtime & Environment
|
|
105
|
+
|
|
106
|
+
* **Python ≥ 3.12** (support the two most recent LTS releases).
|
|
107
|
+
* Create an isolated environment:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
python -m venv .venv
|
|
111
|
+
source .venv/bin/activate
|
|
112
|
+
pip install -e ".[dev]"
|
|
113
|
+
```
|
|
114
|
+
* Enforce `pre‑commit` hooks (flake8, mypy, black, pytest).
|
|
115
|
+
|
|
116
|
+
## 2.2 Core Libraries
|
|
117
|
+
|
|
118
|
+
Mandatory: pydantic, structlog, pytest, pytest-spec, pytest-cov, pytest-mock, flake8, black,
|
|
119
|
+
pre‑commit, mkdocs‑material. Add new libs only when they eliminate **significant** boilerplate or
|
|
120
|
+
risk.
|
|
121
|
+
|
|
122
|
+
## 2.3 Project Structure & Imports
|
|
123
|
+
|
|
124
|
+
* **src‑layout**: code in `src/<package_name>/`; tests live beside code as `*_spec.py`.
|
|
125
|
+
* Import order: 1) stdlib, 2) third‑party, 3) first‑party — each group alphabetised with a blank
|
|
126
|
+
line.
|
|
127
|
+
|
|
128
|
+
## 2.4 Naming & Style
|
|
129
|
+
|
|
130
|
+
* `snake_case` for functions & vars, `PascalCase` for classes, `UPPER_SNAKE` for constants.
|
|
131
|
+
* Prefix intentionally unused vars/args with `_`.
|
|
132
|
+
* **flake8** (with plugins) handles linting, and **black** auto‑formats code. Max line length
|
|
133
|
+
**100**.
|
|
134
|
+
* Cyclomatic complexity cap: **10** (flake8 `C901`).
|
|
135
|
+
* Use **f‑strings**; avoid magic numbers.
|
|
136
|
+
|
|
137
|
+
## 2.5 Type Hints & Docstrings
|
|
138
|
+
|
|
139
|
+
* **100% type coverage**; code must pass `mypy --strict`.
|
|
140
|
+
* Use `pydantic.BaseModel` for data models; don't use bare `@dataclass` if validation is needed.
|
|
141
|
+
* Docstrings in Google format; omit the obvious.
|
|
142
|
+
|
|
143
|
+
## 2.6 Logging & Observability
|
|
144
|
+
|
|
145
|
+
* Configure **structlog** for JSON output by default.
|
|
146
|
+
* Never use `print` for diagnostics; reserve for user‑facing CLI UX.
|
|
147
|
+
* Log levels: `DEBUG` (dev detail) → `INFO` (lifecycle) → `WARNING` (recoverable) → `ERROR` (user
|
|
148
|
+
visible).
|
|
149
|
+
|
|
150
|
+
## 2.7 Testing Strategy
|
|
151
|
+
|
|
152
|
+
* **pytest** with **pytest-spec** for specification-style output.
|
|
153
|
+
* Test files end with `_spec.py` and live in the same folder as the code under test.
|
|
154
|
+
* Use **Arrange / Act / Assert** blocks separated by a blank line (no comments) **or** BDD
|
|
155
|
+
`describe/should` classes.
|
|
156
|
+
* Function names: use `should_*` and BDD-style specifications.
|
|
157
|
+
* Class names: use `Describe*` and BDD-style test suites.
|
|
158
|
+
* **Mocking**: Use `pytest-mock`'s `mocker` fixture; don't use `unittest.mock.MagicMock` directly.
|
|
159
|
+
* One behavioural expectation per test. Fixtures are helpers, not magic.
|
|
160
|
+
* Tests should fail for one reason, avoid multiple assert statements, split the test cases
|
|
161
|
+
|
|
162
|
+
# 3 - Planning and Goal Tracking
|
|
163
|
+
|
|
164
|
+
- Use the provided task manager tools to create your plans and work through them step by step.
|
|
165
|
+
- Before declaring yourself finished list all tasks, ensure they are all complete, and that you
|
|
166
|
+
have not missed any steps
|
|
167
|
+
- If you've missed or forgotten some steps, add them to the task list and continue
|
|
168
|
+
- When all tasks are complete, and you can think of no more to add, declare yourself finished.
|
|
169
|
+
"""
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Define the task
|
|
173
|
+
task = """
|
|
174
|
+
Create a new Python project that is a graphical clone of Windows MineSweeper.
|
|
175
|
+
|
|
176
|
+
Ensure it is well tested.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
# Solve the task and print the result
|
|
180
|
+
result = solver.solve(task)
|
|
181
|
+
print(result)
|
_examples/file_tool.py
CHANGED
|
@@ -5,7 +5,7 @@ from mojentic.agents.output_agent import OutputAgent
|
|
|
5
5
|
from mojentic.dispatcher import Dispatcher
|
|
6
6
|
from mojentic.event import Event
|
|
7
7
|
from mojentic.llm.llm_broker import LLMBroker
|
|
8
|
-
from mojentic.llm.tools.file_manager import ReadFileTool, WriteFileTool
|
|
8
|
+
from mojentic.llm.tools.file_manager import ReadFileTool, WriteFileTool, FilesystemGateway
|
|
9
9
|
from mojentic.router import Router
|
|
10
10
|
|
|
11
11
|
|
|
@@ -25,8 +25,10 @@ class RequestAgent(BaseLLMAgent):
|
|
|
25
25
|
def __init__(self, llm: LLMBroker):
|
|
26
26
|
super().__init__(llm,
|
|
27
27
|
"You are a helpful assistant.")
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# Create a filesystem gateway for the /tmp directory
|
|
29
|
+
fs = FilesystemGateway(base_path="/tmp")
|
|
30
|
+
self.add_tool(ReadFileTool(fs))
|
|
31
|
+
self.add_tool(WriteFileTool(fs))
|
|
30
32
|
|
|
31
33
|
def receive_event(self, event):
|
|
32
34
|
response = self.generate_response(event.text)
|
mojentic/__init__.py
CHANGED
|
@@ -7,10 +7,12 @@ import logging
|
|
|
7
7
|
|
|
8
8
|
import structlog
|
|
9
9
|
|
|
10
|
+
# Core components
|
|
10
11
|
from .dispatcher import Dispatcher
|
|
11
12
|
from .event import Event
|
|
12
13
|
from .router import Router
|
|
13
14
|
|
|
15
|
+
# Initialize logging
|
|
14
16
|
logging.basicConfig(level=logging.INFO)
|
|
15
17
|
structlog.configure(logger_factory=structlog.stdlib.LoggerFactory(), processors=[
|
|
16
18
|
structlog.stdlib.filter_by_level,
|
|
@@ -22,13 +24,6 @@ structlog.configure(logger_factory=structlog.stdlib.LoggerFactory(), processors=
|
|
|
22
24
|
logger = structlog.get_logger()
|
|
23
25
|
logger.info("Starting logger")
|
|
24
26
|
|
|
25
|
-
# logger = logging.getLogger("mojentic")
|
|
26
|
-
# path = Path().cwd()
|
|
27
|
-
# log_filename = path / 'output.log'
|
|
28
|
-
# print(f"Logging to {log_filename}")
|
|
29
|
-
# logging.basicConfig(filename=log_filename, level=logging.DEBUG)
|
|
30
|
-
# logger.info("Starting logger")
|
|
31
|
-
|
|
32
27
|
|
|
33
28
|
__version__: str
|
|
34
29
|
try:
|
mojentic/agents/__init__.py
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mojentic agents module for creating and working with various agent types.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Base agent types
|
|
1
6
|
from .base_agent import BaseAgent
|
|
2
|
-
from .base_llm_agent import BaseLLMAgent
|
|
3
|
-
|
|
7
|
+
from .base_llm_agent import BaseLLMAgent, BaseLLMAgentWithMemory
|
|
8
|
+
|
|
9
|
+
# Special purpose agents
|
|
4
10
|
from .correlation_aggregator_agent import BaseAggregatingAgent
|
|
5
11
|
from .output_agent import OutputAgent
|
|
6
12
|
from .iterative_problem_solver import IterativeProblemSolver
|
|
7
13
|
from .simple_recursive_agent import SimpleRecursiveAgent
|
|
14
|
+
|
|
15
|
+
# Agent brokering
|
|
16
|
+
from .agent_broker import AgentBroker
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import structlog
|
|
3
|
+
|
|
4
|
+
from mojentic.agents.base_async_agent import BaseAsyncAgent
|
|
5
|
+
from mojentic.event import Event
|
|
6
|
+
|
|
7
|
+
logger = structlog.get_logger()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AsyncAggregatorAgent(BaseAsyncAgent):
|
|
11
|
+
"""
|
|
12
|
+
AsyncAggregatorAgent is an asynchronous version of the BaseAggregatingAgent.
|
|
13
|
+
It aggregates events based on their correlation_id and processes them when all required events are available.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, event_types_needed=None):
|
|
16
|
+
"""
|
|
17
|
+
Initialize the AsyncAggregatorAgent.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
event_types_needed : list, optional
|
|
22
|
+
List of event types that need to be captured before processing
|
|
23
|
+
"""
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.results = {}
|
|
26
|
+
self.event_types_needed = event_types_needed or []
|
|
27
|
+
self.futures = {} # Maps correlation_id to Future objects
|
|
28
|
+
|
|
29
|
+
async def _get_and_reset_results(self, event):
|
|
30
|
+
"""
|
|
31
|
+
Get and reset the results for a specific correlation_id.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
event : Event
|
|
36
|
+
The event to get results for
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
list
|
|
41
|
+
The results for the event
|
|
42
|
+
"""
|
|
43
|
+
results = self.results[event.correlation_id]
|
|
44
|
+
self.results[event.correlation_id] = None
|
|
45
|
+
return results
|
|
46
|
+
|
|
47
|
+
async def _capture_results_if_needed(self, event):
|
|
48
|
+
"""
|
|
49
|
+
Capture results for a specific correlation_id.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
event : Event
|
|
54
|
+
The event to capture results for
|
|
55
|
+
"""
|
|
56
|
+
results = self.results.get(event.correlation_id, [])
|
|
57
|
+
results.append(event)
|
|
58
|
+
self.results[event.correlation_id] = results
|
|
59
|
+
|
|
60
|
+
# Check if we have all needed events and set the future if we do
|
|
61
|
+
event_types_captured = [type(e) for e in self.results.get(event.correlation_id, [])]
|
|
62
|
+
finished = all([event_type in event_types_captured for event_type in self.event_types_needed])
|
|
63
|
+
|
|
64
|
+
if finished and event.correlation_id in self.futures:
|
|
65
|
+
future = self.futures[event.correlation_id]
|
|
66
|
+
if not future.done():
|
|
67
|
+
future.set_result(self.results[event.correlation_id])
|
|
68
|
+
|
|
69
|
+
async def _has_all_needed(self, event):
|
|
70
|
+
"""
|
|
71
|
+
Check if all needed event types have been captured for a specific correlation_id.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
event : Event
|
|
76
|
+
The event to check
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
bool
|
|
81
|
+
True if all needed event types have been captured, False otherwise
|
|
82
|
+
"""
|
|
83
|
+
event_types_captured = [type(e) for e in self.results.get(event.correlation_id, [])]
|
|
84
|
+
finished = all([event_type in event_types_captured for event_type in self.event_types_needed])
|
|
85
|
+
logger.debug(f"Captured: {event_types_captured}, Needed: {self.event_types_needed}, Finished: {finished}")
|
|
86
|
+
return finished
|
|
87
|
+
|
|
88
|
+
async def wait_for_events(self, correlation_id, timeout=None):
|
|
89
|
+
"""
|
|
90
|
+
Wait for all needed events for a specific correlation_id.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
correlation_id : str
|
|
95
|
+
The correlation_id to wait for
|
|
96
|
+
timeout : float, optional
|
|
97
|
+
The timeout in seconds
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
list
|
|
102
|
+
The events for the correlation_id
|
|
103
|
+
"""
|
|
104
|
+
if correlation_id not in self.futures:
|
|
105
|
+
self.futures[correlation_id] = asyncio.Future()
|
|
106
|
+
|
|
107
|
+
# If we already have all needed events, return them
|
|
108
|
+
if correlation_id in self.results:
|
|
109
|
+
event_types_captured = [type(e) for e in self.results.get(correlation_id, [])]
|
|
110
|
+
if all([event_type in event_types_captured for event_type in self.event_types_needed]):
|
|
111
|
+
return self.results[correlation_id]
|
|
112
|
+
|
|
113
|
+
# Otherwise, wait for the future to be set
|
|
114
|
+
try:
|
|
115
|
+
return await asyncio.wait_for(self.futures[correlation_id], timeout)
|
|
116
|
+
except asyncio.TimeoutError:
|
|
117
|
+
logger.warning(f"Timeout waiting for events for correlation_id {correlation_id}")
|
|
118
|
+
return self.results.get(correlation_id, [])
|
|
119
|
+
|
|
120
|
+
async def receive_event_async(self, event: Event) -> list:
|
|
121
|
+
"""
|
|
122
|
+
Receive an event and process it if all needed events are available.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
event : Event
|
|
127
|
+
The event to process
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
list
|
|
132
|
+
The events to be processed next
|
|
133
|
+
"""
|
|
134
|
+
# First capture the event
|
|
135
|
+
await self._capture_results_if_needed(event)
|
|
136
|
+
|
|
137
|
+
# Then check if we have all needed events
|
|
138
|
+
event_types_captured = [type(e) for e in self.results.get(event.correlation_id, [])]
|
|
139
|
+
finished = all([event_type in event_types_captured for event_type in self.event_types_needed])
|
|
140
|
+
|
|
141
|
+
# If we have all needed events, process them
|
|
142
|
+
if finished:
|
|
143
|
+
return await self.process_events(await self._get_and_reset_results(event))
|
|
144
|
+
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
async def process_events(self, events):
|
|
148
|
+
"""
|
|
149
|
+
Process a list of events.
|
|
150
|
+
This method should be overridden by subclasses.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
events : list
|
|
155
|
+
The events to process
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
list
|
|
160
|
+
The events to be processed next
|
|
161
|
+
"""
|
|
162
|
+
return []
|