phronesis-framework 0.1.0__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.
Files changed (199) hide show
  1. phronesis/__init__.py +38 -0
  2. phronesis/_internal/__init__.py +1 -0
  3. phronesis/_internal/concurrency/__init__.py +18 -0
  4. phronesis/_internal/concurrency/exceptions.py +39 -0
  5. phronesis/_internal/concurrency/executor.py +71 -0
  6. phronesis/_internal/concurrency/gather.py +65 -0
  7. phronesis/_internal/concurrency/policies.py +70 -0
  8. phronesis/_internal/http/__init__.py +35 -0
  9. phronesis/_internal/http/client.py +429 -0
  10. phronesis/_internal/http/exceptions.py +61 -0
  11. phronesis/_internal/http/headers.py +34 -0
  12. phronesis/_internal/http/models.py +33 -0
  13. phronesis/_internal/http/timeouts.py +29 -0
  14. phronesis/_internal/ids/__init__.py +13 -0
  15. phronesis/_internal/ids/derivation.py +10 -0
  16. phronesis/_internal/ids/generator.py +24 -0
  17. phronesis/_internal/ids/id.py +37 -0
  18. phronesis/_internal/ids/validator.py +26 -0
  19. phronesis/_internal/logging/__init__.py +20 -0
  20. phronesis/_internal/logging/adapter.py +30 -0
  21. phronesis/_internal/logging/config.py +49 -0
  22. phronesis/_internal/logging/constants.py +12 -0
  23. phronesis/_internal/logging/factory.py +18 -0
  24. phronesis/_internal/logging/formatters.py +94 -0
  25. phronesis/_internal/retry/__init__.py +18 -0
  26. phronesis/_internal/retry/attempt.py +18 -0
  27. phronesis/_internal/retry/backoff.py +46 -0
  28. phronesis/_internal/retry/decorator.py +191 -0
  29. phronesis/_internal/retry/exceptions.py +27 -0
  30. phronesis/_internal/typing/__init__.py +45 -0
  31. phronesis/_internal/typing/binary.py +13 -0
  32. phronesis/_internal/typing/json.py +9 -0
  33. phronesis/_internal/typing/maybe.py +36 -0
  34. phronesis/_internal/typing/newtypes.py +11 -0
  35. phronesis/_internal/typing/protocols.py +22 -0
  36. phronesis/_internal/typing/result.py +27 -0
  37. phronesis/_internal/typing/sentinels.py +21 -0
  38. phronesis/_internal/typing/streaming.py +21 -0
  39. phronesis/agents/__init__.py +81 -0
  40. phronesis/agents/agent.py +292 -0
  41. phronesis/agents/decorator.py +200 -0
  42. phronesis/agents/errors.py +100 -0
  43. phronesis/agents/events.py +137 -0
  44. phronesis/agents/hooks.py +78 -0
  45. phronesis/agents/id.py +35 -0
  46. phronesis/agents/loop.py +883 -0
  47. phronesis/agents/registry.py +165 -0
  48. phronesis/agents/run.py +141 -0
  49. phronesis/agents/session.py +122 -0
  50. phronesis/agents/spec.py +88 -0
  51. phronesis/agents/validation.py +128 -0
  52. phronesis/communication/__init__.py +1 -0
  53. phronesis/communication/session_id.py +16 -0
  54. phronesis/context/__init__.py +41 -0
  55. phronesis/context/budget.py +17 -0
  56. phronesis/context/chain.py +101 -0
  57. phronesis/context/compacting.py +387 -0
  58. phronesis/context/context.py +50 -0
  59. phronesis/context/default.py +55 -0
  60. phronesis/context/dry_run.py +88 -0
  61. phronesis/context/errors.py +30 -0
  62. phronesis/context/input.py +42 -0
  63. phronesis/context/protocol.py +45 -0
  64. phronesis/core/__init__.py +36 -0
  65. phronesis/core/messages.py +251 -0
  66. phronesis/errors.py +36 -0
  67. phronesis/mcp/__init__.py +62 -0
  68. phronesis/mcp/_adapt.py +211 -0
  69. phronesis/mcp/client.py +165 -0
  70. phronesis/mcp/errors.py +58 -0
  71. phronesis/mcp/ids.py +50 -0
  72. phronesis/mcp/obs.py +52 -0
  73. phronesis/mcp/server.py +229 -0
  74. phronesis/mcp/server_spec.py +65 -0
  75. phronesis/mcp/transport.py +53 -0
  76. phronesis/memory/__init__.py +98 -0
  77. phronesis/memory/checkpoint.py +201 -0
  78. phronesis/memory/context_builder.py +194 -0
  79. phronesis/memory/episodic/__init__.py +22 -0
  80. phronesis/memory/episodic/filesystem.py +172 -0
  81. phronesis/memory/episodic/in_memory.py +92 -0
  82. phronesis/memory/episodic/protocol.py +81 -0
  83. phronesis/memory/errors.py +50 -0
  84. phronesis/memory/hooks.py +89 -0
  85. phronesis/memory/kv/__init__.py +21 -0
  86. phronesis/memory/kv/filesystem.py +247 -0
  87. phronesis/memory/kv/in_memory.py +175 -0
  88. phronesis/memory/kv/protocol.py +70 -0
  89. phronesis/memory/obs.py +100 -0
  90. phronesis/memory/scope.py +111 -0
  91. phronesis/memory/tools.py +296 -0
  92. phronesis/memory/vector/__init__.py +28 -0
  93. phronesis/memory/vector/_cosine.py +34 -0
  94. phronesis/memory/vector/embeddings.py +69 -0
  95. phronesis/memory/vector/filesystem.py +174 -0
  96. phronesis/memory/vector/in_memory.py +102 -0
  97. phronesis/memory/vector/protocol.py +94 -0
  98. phronesis/memory/working.py +165 -0
  99. phronesis/middleware/__init__.py +30 -0
  100. phronesis/middleware/chain.py +102 -0
  101. phronesis/middleware/errors.py +13 -0
  102. phronesis/middleware/protocol.py +58 -0
  103. phronesis/obs/__init__.py +49 -0
  104. phronesis/obs/_detect.py +28 -0
  105. phronesis/obs/_noop.py +71 -0
  106. phronesis/obs/attributes.py +82 -0
  107. phronesis/obs/config.py +257 -0
  108. phronesis/obs/errors.py +25 -0
  109. phronesis/obs/logging_filter.py +97 -0
  110. phronesis/obs/metrics.py +146 -0
  111. phronesis/obs/spans.py +165 -0
  112. phronesis/pipelines/__init__.py +28 -0
  113. phronesis/pipelines/errors.py +21 -0
  114. phronesis/pipelines/ids.py +63 -0
  115. phronesis/pipelines/pipeline.py +250 -0
  116. phronesis/providers/__init__.py +80 -0
  117. phronesis/providers/_common/__init__.py +9 -0
  118. phronesis/providers/anthropic/__init__.py +13 -0
  119. phronesis/providers/anthropic/errors.py +113 -0
  120. phronesis/providers/anthropic/factory.py +81 -0
  121. phronesis/providers/anthropic/messages.py +248 -0
  122. phronesis/providers/anthropic/provider.py +344 -0
  123. phronesis/providers/anthropic/streaming.py +305 -0
  124. phronesis/providers/anthropic/tools.py +48 -0
  125. phronesis/providers/chunks.py +85 -0
  126. phronesis/providers/errors.py +98 -0
  127. phronesis/providers/fallback.py +145 -0
  128. phronesis/providers/openai/__init__.py +20 -0
  129. phronesis/providers/openai/errors.py +101 -0
  130. phronesis/providers/openai/factory.py +98 -0
  131. phronesis/providers/openai/helpers.py +189 -0
  132. phronesis/providers/openai/messages.py +180 -0
  133. phronesis/providers/openai/provider.py +403 -0
  134. phronesis/providers/openai/streaming.py +275 -0
  135. phronesis/providers/openai/tools.py +50 -0
  136. phronesis/providers/protocol.py +156 -0
  137. phronesis/providers/retry_config.py +81 -0
  138. phronesis/providers/translation.py +167 -0
  139. phronesis/providers/types.py +186 -0
  140. phronesis/providers/usage.py +32 -0
  141. phronesis/replay/__init__.py +57 -0
  142. phronesis/replay/cassette.py +148 -0
  143. phronesis/replay/errors.py +23 -0
  144. phronesis/replay/recording.py +82 -0
  145. phronesis/replay/replay.py +127 -0
  146. phronesis/runtime/__init__.py +99 -0
  147. phronesis/runtime/context.py +153 -0
  148. phronesis/runtime/errors.py +77 -0
  149. phronesis/runtime/modes/__init__.py +5 -0
  150. phronesis/runtime/modes/approval.py +78 -0
  151. phronesis/runtime/modes/cascade.py +55 -0
  152. phronesis/runtime/modes/conditional.py +41 -0
  153. phronesis/runtime/modes/consensus.py +100 -0
  154. phronesis/runtime/modes/debate.py +80 -0
  155. phronesis/runtime/modes/fallback.py +50 -0
  156. phronesis/runtime/modes/handoff_chain.py +100 -0
  157. phronesis/runtime/modes/loop.py +80 -0
  158. phronesis/runtime/modes/map_reduce.py +81 -0
  159. phronesis/runtime/modes/parallel.py +82 -0
  160. phronesis/runtime/modes/plan_and_execute.py +79 -0
  161. phronesis/runtime/modes/race.py +90 -0
  162. phronesis/runtime/modes/reflexion.py +116 -0
  163. phronesis/runtime/modes/retry.py +83 -0
  164. phronesis/runtime/modes/router.py +57 -0
  165. phronesis/runtime/modes/sequence.py +59 -0
  166. phronesis/runtime/modes/supervisor.py +113 -0
  167. phronesis/runtime/modes/tree_search.py +151 -0
  168. phronesis/runtime/modes/validation.py +88 -0
  169. phronesis/runtime/node.py +150 -0
  170. phronesis/runtime/obs.py +95 -0
  171. phronesis/runtime/outcome.py +147 -0
  172. phronesis/runtime/protocol.py +36 -0
  173. phronesis/testing/__init__.py +19 -0
  174. phronesis/testing/providers.py +159 -0
  175. phronesis/tools/__init__.py +76 -0
  176. phronesis/tools/cache.py +141 -0
  177. phronesis/tools/decorator.py +184 -0
  178. phronesis/tools/discover.py +48 -0
  179. phronesis/tools/effects.py +46 -0
  180. phronesis/tools/errors.py +262 -0
  181. phronesis/tools/injection.py +82 -0
  182. phronesis/tools/lifecycle.py +47 -0
  183. phronesis/tools/markers.py +39 -0
  184. phronesis/tools/providers/__init__.py +47 -0
  185. phronesis/tools/providers/anthropic.py +52 -0
  186. phronesis/tools/providers/base.py +52 -0
  187. phronesis/tools/providers/openai.py +56 -0
  188. phronesis/tools/registry.py +148 -0
  189. phronesis/tools/retry.py +65 -0
  190. phronesis/tools/schema.py +216 -0
  191. phronesis/tools/single_model.py +92 -0
  192. phronesis/tools/spec.py +106 -0
  193. phronesis/tools/tool.py +317 -0
  194. phronesis/tools/tool_id.py +44 -0
  195. phronesis/tools/validation.py +160 -0
  196. phronesis/tools/version.py +108 -0
  197. phronesis_framework-0.1.0.dist-info/METADATA +79 -0
  198. phronesis_framework-0.1.0.dist-info/RECORD +199 -0
  199. phronesis_framework-0.1.0.dist-info/WHEEL +4 -0
phronesis/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ """Phronesis: opinionated framework for building AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging as _logging
6
+
7
+ from ._internal.logging import (
8
+ configure_logging,
9
+ get_logger,
10
+ get_logger_with_context,
11
+ )
12
+ from ._internal.logging.constants import PHRONESIS_LOGGER_PREFIX as _ROOT
13
+ from .context.context import Context
14
+ from .tools.decorator import tool
15
+ from .tools.discover import discover
16
+ from .tools.effects import ToolEffect
17
+ from .tools.errors import ToolError
18
+ from .tools.registry import tool_scope
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ # Defensive: prevent stdlib "no handlers" warnings when the consumer has not
23
+ # configured logging. Users opt in by calling `configure_logging()` or by
24
+ # attaching their own handler to the `phronesis` logger.
25
+ _logging.getLogger(_ROOT).addHandler(_logging.NullHandler())
26
+
27
+ __all__ = [
28
+ "Context",
29
+ "ToolEffect",
30
+ "ToolError",
31
+ "__version__",
32
+ "configure_logging",
33
+ "discover",
34
+ "get_logger",
35
+ "get_logger_with_context",
36
+ "tool",
37
+ "tool_scope",
38
+ ]
@@ -0,0 +1 @@
1
+ """Internal utilities. Not part of the public Phronesis API."""
@@ -0,0 +1,18 @@
1
+ """Concurrency utilities: thread offloading and concurrent task execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .exceptions import ConcurrencyError, PartialFailureError
6
+ from .executor import run_sync
7
+ from .gather import gather_all
8
+ from .policies import BestEffortPolicy, FailFastPolicy, GatherPolicy
9
+
10
+ __all__ = [
11
+ "BestEffortPolicy",
12
+ "ConcurrencyError",
13
+ "FailFastPolicy",
14
+ "GatherPolicy",
15
+ "PartialFailureError",
16
+ "gather_all",
17
+ "run_sync",
18
+ ]
@@ -0,0 +1,39 @@
1
+ """Concurrency-specific exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class ConcurrencyError(Exception):
9
+ """Base class for concurrency-related errors raised by the framework."""
10
+
11
+
12
+ class PartialFailureError(ConcurrencyError):
13
+ """Some tasks in a best-effort gather failed.
14
+
15
+ Successful values and exceptions are kept in the original task order:
16
+ if task ``i`` succeeded, ``results[i]`` is its value and
17
+ ``exceptions[i]`` is ``None``; if it failed, ``results[i]`` is ``None``
18
+ and ``exceptions[i]`` is the captured exception.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ message: str,
24
+ *,
25
+ results: list[Any],
26
+ exceptions: list[BaseException | None],
27
+ ) -> None:
28
+ super().__init__(message)
29
+
30
+ self.results = results
31
+ self.exceptions = exceptions
32
+
33
+ @property
34
+ def failed_count(self) -> int:
35
+ return sum(1 for exc in self.exceptions if exc is not None)
36
+
37
+ @property
38
+ def successful_count(self) -> int:
39
+ return len(self.exceptions) - self.failed_count
@@ -0,0 +1,71 @@
1
+ """Run synchronous callables from async code via a worker thread."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from collections.abc import Callable
8
+ from typing import ParamSpec, TypeVar
9
+
10
+ from ..logging import get_logger
11
+
12
+ P = ParamSpec("P")
13
+ T = TypeVar("T")
14
+
15
+ _LOGGER_NAME = "phronesis.concurrency"
16
+
17
+
18
+ async def run_sync(
19
+ fn: Callable[P, T],
20
+ *args: P.args,
21
+ **kwargs: P.kwargs,
22
+ ) -> T:
23
+ """Run a synchronous callable in a worker thread.
24
+
25
+ Wraps :func:`asyncio.to_thread` with structured logging. The
26
+ callable runs in the default executor.
27
+
28
+ Args:
29
+ fn: The synchronous callable to invoke off the event loop.
30
+ *args: Positional arguments forwarded to ``fn``.
31
+ **kwargs: Keyword arguments forwarded to ``fn``.
32
+
33
+ Returns:
34
+ Whatever ``fn`` returns.
35
+
36
+ Raises:
37
+ Exception: Any exception raised by ``fn`` propagates
38
+ unchanged after a warning log entry.
39
+ """
40
+ log = get_logger(_LOGGER_NAME)
41
+ label = getattr(fn, "__qualname__", repr(fn))
42
+
43
+ log.debug("run_sync start", extra={"callable": label})
44
+
45
+ started = time.perf_counter()
46
+
47
+ try:
48
+ result = await asyncio.to_thread(fn, *args, **kwargs)
49
+
50
+ except Exception as exc:
51
+ duration_ms = (time.perf_counter() - started) * 1000
52
+
53
+ log.warning(
54
+ "run_sync failed",
55
+ extra={
56
+ "callable": label,
57
+ "duration_ms": duration_ms,
58
+ "error": str(exc),
59
+ },
60
+ )
61
+
62
+ raise
63
+
64
+ duration_ms = (time.perf_counter() - started) * 1000
65
+
66
+ log.debug(
67
+ "run_sync done",
68
+ extra={"callable": label, "duration_ms": duration_ms},
69
+ )
70
+
71
+ return result
@@ -0,0 +1,65 @@
1
+ """Concurrent execution of awaitables with configurable error policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from collections.abc import Awaitable
8
+ from typing import TypeVar, cast
9
+
10
+ from ..logging import get_logger
11
+ from .policies import FailFastPolicy, GatherPolicy
12
+
13
+ T = TypeVar("T")
14
+
15
+ _LOGGER_NAME = "phronesis.concurrency"
16
+
17
+
18
+ async def gather_all(
19
+ *awaitables: Awaitable[T],
20
+ policy: GatherPolicy | None = None,
21
+ ) -> list[T]:
22
+ """Run ``awaitables`` concurrently and reconcile via ``policy``.
23
+
24
+ Args:
25
+ *awaitables: Awaitables scheduled on the running event loop.
26
+ policy: Error-handling strategy. Defaults to
27
+ :class:`FailFastPolicy`. Use :class:`BestEffortPolicy` to
28
+ wait for every task and surface partial failures.
29
+
30
+ Returns:
31
+ Results in input order. For :class:`BestEffortPolicy`, failed
32
+ slots are replaced with ``None`` before the policy raises.
33
+
34
+ Raises:
35
+ Exception: The first exception raised by any awaitable under
36
+ :class:`FailFastPolicy`.
37
+ PartialFailureError: When :class:`BestEffortPolicy` finds at
38
+ least one failed awaitable.
39
+ """
40
+ effective_policy = policy or FailFastPolicy()
41
+ log = get_logger(_LOGGER_NAME)
42
+
43
+ log.debug(
44
+ "gather_all start",
45
+ extra={
46
+ "count": len(awaitables),
47
+ "policy": type(effective_policy).__name__,
48
+ },
49
+ )
50
+
51
+ started = time.perf_counter()
52
+
53
+ raw = await asyncio.gather(
54
+ *awaitables,
55
+ return_exceptions=effective_policy.return_exceptions,
56
+ )
57
+
58
+ duration_ms = (time.perf_counter() - started) * 1000
59
+
60
+ log.debug(
61
+ "gather_all done",
62
+ extra={"count": len(awaitables), "duration_ms": duration_ms},
63
+ )
64
+
65
+ return cast("list[T]", effective_policy.reconcile(raw))
@@ -0,0 +1,70 @@
1
+ """Error-handling policies for concurrent task execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Sequence
7
+ from typing import Any
8
+
9
+ from .exceptions import PartialFailureError
10
+
11
+
12
+ class GatherPolicy(ABC):
13
+ """Strategy for handling exceptions returned by :func:`asyncio.gather`."""
14
+
15
+ @property
16
+ @abstractmethod
17
+ def return_exceptions(self) -> bool:
18
+ """Value passed to ``asyncio.gather(return_exceptions=...)``."""
19
+
20
+ @abstractmethod
21
+ def reconcile(self, results: Sequence[Any]) -> list[Any]:
22
+ """Inspect raw gather output and either return successes or raise."""
23
+
24
+
25
+ class FailFastPolicy(GatherPolicy):
26
+ """Cancel pending tasks and propagate the first exception immediately."""
27
+
28
+ @property
29
+ def return_exceptions(self) -> bool:
30
+ return False
31
+
32
+ def reconcile(self, results: Sequence[Any]) -> list[Any]:
33
+ # With return_exceptions=False, asyncio.gather already raised on
34
+ # the first failure, so any sequence we receive is all-successful.
35
+ return list(results)
36
+
37
+
38
+ class BestEffortPolicy(GatherPolicy):
39
+ """Wait for every task; raise :class:`PartialFailureError` if any failed."""
40
+
41
+ @property
42
+ def return_exceptions(self) -> bool:
43
+ return True
44
+
45
+ def reconcile(self, results: Sequence[Any]) -> list[Any]:
46
+ successes: list[Any] = []
47
+ failures: list[BaseException | None] = []
48
+ any_failed = False
49
+
50
+ for item in results:
51
+ if isinstance(item, BaseException):
52
+ successes.append(None)
53
+ failures.append(item)
54
+ any_failed = True
55
+
56
+ else:
57
+ successes.append(item)
58
+ failures.append(None)
59
+
60
+ if any_failed:
61
+ failed = sum(1 for exc in failures if exc is not None)
62
+ total = len(results)
63
+
64
+ raise PartialFailureError(
65
+ f"{failed} of {total} tasks failed",
66
+ results=successes,
67
+ exceptions=failures,
68
+ )
69
+
70
+ return successes
@@ -0,0 +1,35 @@
1
+ """Async HTTP client used by every provider via composition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .client import HttpClient, HttpStreamResponse, configure_http_client
6
+ from .exceptions import (
7
+ HttpClientError,
8
+ HttpConnectionError,
9
+ HttpError,
10
+ HttpResponseError,
11
+ HttpServerError,
12
+ HttpTimeoutError,
13
+ HttpTransportError,
14
+ )
15
+ from .headers import build_default_headers, redact_sensitive_headers
16
+ from .models import HttpRequest, HttpResponse
17
+ from .timeouts import HttpTimeouts
18
+
19
+ __all__ = [
20
+ "HttpClient",
21
+ "HttpClientError",
22
+ "HttpConnectionError",
23
+ "HttpError",
24
+ "HttpRequest",
25
+ "HttpResponse",
26
+ "HttpResponseError",
27
+ "HttpServerError",
28
+ "HttpStreamResponse",
29
+ "HttpTimeoutError",
30
+ "HttpTimeouts",
31
+ "HttpTransportError",
32
+ "build_default_headers",
33
+ "configure_http_client",
34
+ "redact_sensitive_headers",
35
+ ]