penguiflow 2.2.2__tar.gz → 2.2.3__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 penguiflow might be problematic. Click here for more details.
- {penguiflow-2.2.2 → penguiflow-2.2.3}/PKG-INFO +1 -1
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/__init__.py +1 -1
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/core.py +24 -1
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/PKG-INFO +1 -1
- {penguiflow-2.2.2 → penguiflow-2.2.3}/pyproject.toml +1 -1
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_errors.py +37 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/LICENSE +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/README.md +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/admin.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/bus.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/catalog.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/debug.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/errors.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/metrics.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/middlewares.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/node.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/patterns.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/planner/__init__.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/planner/prompts.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/planner/react.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/policies.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/registry.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/remote.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/state.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/streaming.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/testkit.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/types.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow/viz.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/SOURCES.txt +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/dependency_links.txt +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/entry_points.txt +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/requires.txt +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow.egg-info/top_level.txt +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow_a2a/__init__.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/penguiflow_a2a/server.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/setup.cfg +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_a2a_server.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_budgets.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_cancel.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_catalog.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_controller.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_core.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_distribution_hooks.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_metadata.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_metrics.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_middlewares.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_node.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_patterns.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_planner_prompts.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_property_based.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_react_planner.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_registry.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_remote.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_routing_policy.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_streaming.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_testkit.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_types.py +0 -0
- {penguiflow-2.2.2 → penguiflow-2.2.3}/tests/test_viz.py +0 -0
|
@@ -14,6 +14,7 @@ from collections import deque
|
|
|
14
14
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
15
15
|
from contextlib import suppress
|
|
16
16
|
from dataclasses import dataclass
|
|
17
|
+
from types import TracebackType
|
|
17
18
|
from typing import Any, cast
|
|
18
19
|
|
|
19
20
|
from .bus import BusEnvelope, MessageBus
|
|
@@ -27,6 +28,14 @@ from .types import WM, FinalAnswer, Message, StreamChunk
|
|
|
27
28
|
|
|
28
29
|
logger = logging.getLogger("penguiflow.core")
|
|
29
30
|
|
|
31
|
+
ExcInfo = tuple[type[BaseException], BaseException, TracebackType | None]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _capture_exc_info(exc: BaseException | None) -> ExcInfo | None:
|
|
35
|
+
if exc is None:
|
|
36
|
+
return None
|
|
37
|
+
return (type(exc), exc, exc.__traceback__)
|
|
38
|
+
|
|
30
39
|
BUDGET_EXCEEDED_TEXT = "Hop budget exhausted"
|
|
31
40
|
DEADLINE_EXCEEDED_TEXT = "Deadline exceeded"
|
|
32
41
|
TOKEN_BUDGET_EXCEEDED_TEXT = "Token budget exhausted"
|
|
@@ -750,6 +759,7 @@ class PenguiFlow:
|
|
|
750
759
|
raise
|
|
751
760
|
except TimeoutError as exc:
|
|
752
761
|
latency = (time.perf_counter() - start) * 1000
|
|
762
|
+
exc_info = _capture_exc_info(exc)
|
|
753
763
|
await self._emit_event(
|
|
754
764
|
event="node_timeout",
|
|
755
765
|
node=node,
|
|
@@ -759,6 +769,7 @@ class PenguiFlow:
|
|
|
759
769
|
latency_ms=latency,
|
|
760
770
|
level=logging.WARNING,
|
|
761
771
|
extra={"exception": repr(exc)},
|
|
772
|
+
exc_info=exc_info,
|
|
762
773
|
)
|
|
763
774
|
if attempt >= node.policy.max_retries:
|
|
764
775
|
timeout_message: str | None = None
|
|
@@ -783,6 +794,7 @@ class PenguiFlow:
|
|
|
783
794
|
flow_error=flow_error,
|
|
784
795
|
latency=latency,
|
|
785
796
|
attempt=attempt,
|
|
797
|
+
exc_info=exc_info,
|
|
786
798
|
)
|
|
787
799
|
return
|
|
788
800
|
attempt += 1
|
|
@@ -801,6 +813,7 @@ class PenguiFlow:
|
|
|
801
813
|
continue
|
|
802
814
|
except Exception as exc: # noqa: BLE001
|
|
803
815
|
latency = (time.perf_counter() - start) * 1000
|
|
816
|
+
exc_info = _capture_exc_info(exc)
|
|
804
817
|
await self._emit_event(
|
|
805
818
|
event="node_error",
|
|
806
819
|
node=node,
|
|
@@ -810,6 +823,7 @@ class PenguiFlow:
|
|
|
810
823
|
latency_ms=latency,
|
|
811
824
|
level=logging.ERROR,
|
|
812
825
|
extra={"exception": repr(exc)},
|
|
826
|
+
exc_info=exc_info,
|
|
813
827
|
)
|
|
814
828
|
if attempt >= node.policy.max_retries:
|
|
815
829
|
flow_error = self._create_flow_error(
|
|
@@ -830,6 +844,7 @@ class PenguiFlow:
|
|
|
830
844
|
flow_error=flow_error,
|
|
831
845
|
latency=latency,
|
|
832
846
|
attempt=attempt,
|
|
847
|
+
exc_info=exc_info,
|
|
833
848
|
)
|
|
834
849
|
return
|
|
835
850
|
attempt += 1
|
|
@@ -890,6 +905,7 @@ class PenguiFlow:
|
|
|
890
905
|
flow_error: FlowError,
|
|
891
906
|
latency: float | None,
|
|
892
907
|
attempt: int,
|
|
908
|
+
exc_info: ExcInfo | None,
|
|
893
909
|
) -> None:
|
|
894
910
|
original = flow_error.unwrap()
|
|
895
911
|
exception_repr = repr(original) if original is not None else flow_error.message
|
|
@@ -906,6 +922,7 @@ class PenguiFlow:
|
|
|
906
922
|
latency_ms=latency,
|
|
907
923
|
level=logging.ERROR,
|
|
908
924
|
extra=extra,
|
|
925
|
+
exc_info=exc_info,
|
|
909
926
|
)
|
|
910
927
|
if self._emit_errors_to_rookery and flow_error.trace_id is not None:
|
|
911
928
|
await self._emit_to_rookery(flow_error, source=context.owner)
|
|
@@ -1365,6 +1382,7 @@ class PenguiFlow:
|
|
|
1365
1382
|
latency_ms: float | None,
|
|
1366
1383
|
level: int,
|
|
1367
1384
|
extra: dict[str, Any] | None = None,
|
|
1385
|
+
exc_info: ExcInfo | None = None,
|
|
1368
1386
|
) -> None:
|
|
1369
1387
|
node_name = getattr(node, "name", None)
|
|
1370
1388
|
node_id = getattr(node, "node_id", node_name)
|
|
@@ -1398,7 +1416,12 @@ class PenguiFlow:
|
|
|
1398
1416
|
extra=extra or {},
|
|
1399
1417
|
)
|
|
1400
1418
|
|
|
1401
|
-
|
|
1419
|
+
payload = event_obj.to_payload()
|
|
1420
|
+
log_kwargs: dict[str, Any] = {"extra": payload}
|
|
1421
|
+
if exc_info is not None:
|
|
1422
|
+
log_kwargs["exc_info"] = exc_info
|
|
1423
|
+
|
|
1424
|
+
logger.log(level, event, **log_kwargs)
|
|
1402
1425
|
|
|
1403
1426
|
if self._state_store is not None:
|
|
1404
1427
|
stored_event = StoredEvent.from_flow_event(event_obj)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import logging
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
@@ -104,3 +105,39 @@ async def test_flow_error_metadata_for_timeouts() -> None:
|
|
|
104
105
|
assert result.metadata["attempt"] == 0
|
|
105
106
|
|
|
106
107
|
await flow.stop()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.asyncio
|
|
111
|
+
async def test_node_error_logs_exc_info(caplog: pytest.LogCaptureFixture) -> None:
|
|
112
|
+
async def broken(_message: str, _ctx) -> None:
|
|
113
|
+
raise RuntimeError("dependency missing")
|
|
114
|
+
|
|
115
|
+
node = Node(
|
|
116
|
+
broken,
|
|
117
|
+
name="broken",
|
|
118
|
+
policy=NodePolicy(validate="none", max_retries=0),
|
|
119
|
+
)
|
|
120
|
+
flow = create(node.to(), emit_errors_to_rookery=True)
|
|
121
|
+
flow.run()
|
|
122
|
+
|
|
123
|
+
msg = Message(payload="payload", headers=Headers(tenant="demo"))
|
|
124
|
+
|
|
125
|
+
with caplog.at_level(logging.ERROR, logger="penguiflow.core"):
|
|
126
|
+
await flow.emit(msg)
|
|
127
|
+
await asyncio.wait_for(flow.fetch(), timeout=0.5)
|
|
128
|
+
|
|
129
|
+
node_error_records = [
|
|
130
|
+
record for record in caplog.records if record.message == "node_error"
|
|
131
|
+
]
|
|
132
|
+
assert node_error_records, "expected node_error log entry"
|
|
133
|
+
assert node_error_records[0].exc_info is not None
|
|
134
|
+
assert isinstance(node_error_records[0].exc_info[1], RuntimeError)
|
|
135
|
+
|
|
136
|
+
node_failed_records = [
|
|
137
|
+
record for record in caplog.records if record.message == "node_failed"
|
|
138
|
+
]
|
|
139
|
+
assert node_failed_records, "expected node_failed log entry"
|
|
140
|
+
assert node_failed_records[0].exc_info is not None
|
|
141
|
+
assert isinstance(node_failed_records[0].exc_info[1], RuntimeError)
|
|
142
|
+
|
|
143
|
+
await flow.stop()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|