deepharness 0.1.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.
Files changed (48) hide show
  1. deepharness-0.1.0/LICENSE +21 -0
  2. deepharness-0.1.0/PKG-INFO +194 -0
  3. deepharness-0.1.0/README.md +162 -0
  4. deepharness-0.1.0/deepharness/__init__.py +101 -0
  5. deepharness-0.1.0/deepharness/agent/__init__.py +33 -0
  6. deepharness-0.1.0/deepharness/agent/loop.py +414 -0
  7. deepharness-0.1.0/deepharness/agent/output.py +173 -0
  8. deepharness-0.1.0/deepharness/agent/state.py +248 -0
  9. deepharness-0.1.0/deepharness/agent/turn.py +131 -0
  10. deepharness-0.1.0/deepharness/errors.py +102 -0
  11. deepharness-0.1.0/deepharness/graph/__init__.py +14 -0
  12. deepharness-0.1.0/deepharness/graph/builder.py +186 -0
  13. deepharness-0.1.0/deepharness/graph/executor.py +128 -0
  14. deepharness-0.1.0/deepharness/graph/reducers.py +43 -0
  15. deepharness-0.1.0/deepharness/providers/__init__.py +51 -0
  16. deepharness-0.1.0/deepharness/providers/anthropic.py +293 -0
  17. deepharness-0.1.0/deepharness/providers/base.py +203 -0
  18. deepharness-0.1.0/deepharness/providers/client.py +121 -0
  19. deepharness-0.1.0/deepharness/providers/gateways.py +99 -0
  20. deepharness-0.1.0/deepharness/providers/gemini.py +232 -0
  21. deepharness-0.1.0/deepharness/providers/openai.py +281 -0
  22. deepharness-0.1.0/deepharness/providers/rest.py +189 -0
  23. deepharness-0.1.0/deepharness/providers/wire.py +79 -0
  24. deepharness-0.1.0/deepharness/py.typed +0 -0
  25. deepharness-0.1.0/deepharness/tools/__init__.py +3 -0
  26. deepharness-0.1.0/deepharness/tools/toolbox.py +248 -0
  27. deepharness-0.1.0/deepharness.egg-info/PKG-INFO +194 -0
  28. deepharness-0.1.0/deepharness.egg-info/SOURCES.txt +46 -0
  29. deepharness-0.1.0/deepharness.egg-info/dependency_links.txt +1 -0
  30. deepharness-0.1.0/deepharness.egg-info/requires.txt +10 -0
  31. deepharness-0.1.0/deepharness.egg-info/top_level.txt +1 -0
  32. deepharness-0.1.0/pyproject.toml +48 -0
  33. deepharness-0.1.0/setup.cfg +4 -0
  34. deepharness-0.1.0/tests/test_agent.py +713 -0
  35. deepharness-0.1.0/tests/test_approval.py +178 -0
  36. deepharness-0.1.0/tests/test_budget.py +70 -0
  37. deepharness-0.1.0/tests/test_client.py +91 -0
  38. deepharness-0.1.0/tests/test_ctx.py +133 -0
  39. deepharness-0.1.0/tests/test_executor.py +377 -0
  40. deepharness-0.1.0/tests/test_gateways.py +66 -0
  41. deepharness-0.1.0/tests/test_graph.py +175 -0
  42. deepharness-0.1.0/tests/test_message.py +67 -0
  43. deepharness-0.1.0/tests/test_output.py +195 -0
  44. deepharness-0.1.0/tests/test_providers.py +663 -0
  45. deepharness-0.1.0/tests/test_session.py +29 -0
  46. deepharness-0.1.0/tests/test_state.py +67 -0
  47. deepharness-0.1.0/tests/test_streaming.py +394 -0
  48. deepharness-0.1.0/tests/test_toolbox.py +201 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sayed Shaun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepharness
3
+ Version: 0.1.0
4
+ Summary: Compose LLM agents into typed, concurrent workflows.
5
+ Author-email: Sayed Shaun <sayedshaun4@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sayedshaun/deepharness
8
+ Project-URL: Documentation, https://sayedshaun.github.io/deepharness/
9
+ Project-URL: Repository, https://github.com/sayedshaun/deepharness
10
+ Project-URL: Issues, https://github.com/sayedshaun/deepharness/issues
11
+ Keywords: agents,llm,workflow,graph,async
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: httpx
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: pytest-asyncio; extra == "dev"
27
+ Requires-Dist: ruff; extra == "dev"
28
+ Provides-Extra: docs
29
+ Requires-Dist: mkdocs; extra == "docs"
30
+ Requires-Dist: mkdocs-material; extra == "docs"
31
+ Dynamic: license-file
32
+
33
+ <div align="center">
34
+
35
+ <img src="docs/assets/logo.svg" width="160" alt="DeepHarness logo">
36
+
37
+ # DeepHarness
38
+
39
+ **Compose LLM agents into typed, concurrent workflows.**
40
+
41
+ A lightweight framework for wiring plain Python functions — and the agents inside them — into a
42
+ graph that runs branches in parallel, routes on conditions, and threads one typed state
43
+ object through the whole thing.
44
+
45
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![Dependencies: httpx only](https://img.shields.io/badge/dependencies-httpx%20only-6E63F5)](pyproject.toml) [![Async native](https://img.shields.io/badge/async-native-0EA5E9)](#quickstart) [![Ruff](https://img.shields.io/badge/lint-ruff-D7FF64?logo=ruff&logoColor=black)](https://github.com/astral-sh/ruff) [![Read the docs](https://img.shields.io/badge/docs-read%20the%20docs-3776AB?logo=materialformkdocs&logoColor=white)](https://sayedshaun.github.io/deepharness/)
46
+
47
+ </div>
48
+
49
+ ---
50
+
51
+ ## Why DeepHarness
52
+
53
+ - **One dependency. That's it.** `httpx`. Providers talk to vendor REST APIs
54
+ directly — no vendor SDKs, no transitive dependency sprawl.
55
+ - **Your state is a dataclass.** Not an untyped dict. Your editor autocompletes it, your type
56
+ checker checks it.
57
+ - **Nodes are just functions.** Sync or async, decorated in place. No base class to inherit,
58
+ no runner object to construct.
59
+ - **Parallel by default.** Independent branches run concurrently via `asyncio`; sync functions
60
+ are offloaded to threads so a blocking call never stalls the event loop.
61
+ - **One provider interface, a dozen vendors.** `OpenAI`, `Anthropic`, `Gemini`, and OpenAI-compatible
62
+ gateways (`Groq`, `Together`, `Fireworks`, `DeepSeek`, `Mistral`, `Cerebras`, `OpenRouter`, `XAI`,
63
+ `Ollama`, `LMStudio`, `VLLM`) all share the same `LLM` interface.
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ pip install deepharness
69
+ ```
70
+
71
+ Or install from source:
72
+
73
+ ```bash
74
+ pip install git+https://github.com/sayedshaun/deepharness.git
75
+ ```
76
+
77
+ ## Quickstart
78
+
79
+ An `Agent` runs a think/act loop against a model: ask for a response, dispatch any tool calls
80
+ it requests, repeat until the model answers with no tool calls.
81
+
82
+ ```python
83
+ from deepharness import Agent, Message, OpenAI, tool
84
+
85
+
86
+ @tool
87
+ def get_weather(city: str) -> str:
88
+ """Look up the current weather for a city."""
89
+ return f"It is 22°C and sunny in {city}."
90
+
91
+
92
+ agent = Agent(
93
+ OpenAI(model="gpt-4o-mini", api_key="sk-..."),
94
+ tools=[get_weather],
95
+ )
96
+
97
+ result = await agent.arun("Weather in Oslo?")
98
+ print(result.output)
99
+ ```
100
+
101
+ An `Agent` can itself be handed to another agent as a tool via `as_tool()`, so one agent can
102
+ delegate a task to another:
103
+
104
+ ```python
105
+ researcher = Agent(OpenAI(model="gpt-4o-mini", api_key="sk-..."), name="researcher")
106
+
107
+ editor = Agent(
108
+ OpenAI(model="gpt-4o-mini", api_key="sk-..."),
109
+ name="editor",
110
+ tools=[researcher.as_tool(description="Look up facts on a topic.")],
111
+ )
112
+ ```
113
+
114
+ ## Graphs
115
+
116
+ For fixed, known-shape workflows — run these steps, some in parallel, then merge — wire plain
117
+ functions (or agents) into a `Graph` instead:
118
+
119
+ ```python
120
+ import asyncio
121
+ from dataclasses import dataclass
122
+
123
+ from deepharness import Graph
124
+
125
+
126
+ @dataclass
127
+ class State:
128
+ sales: str = ""
129
+ churn: str = ""
130
+ summary: str = ""
131
+
132
+
133
+ graph = Graph(State)
134
+
135
+
136
+ @graph.add(start=True)
137
+ async def fetch_sales(state: State) -> State:
138
+ await asyncio.sleep(0.5)
139
+ state.sales = "Sales up 12% QoQ."
140
+ return state
141
+
142
+
143
+ @graph.add(start=True)
144
+ async def fetch_churn(state: State) -> State:
145
+ await asyncio.sleep(0.3)
146
+ state.churn = "Churn down to 4%."
147
+ return state
148
+
149
+
150
+ @graph.add(end=True)
151
+ def summarize(state: State) -> State:
152
+ state.summary = f"{state.sales} {state.churn}"
153
+ return state
154
+
155
+
156
+ graph.connect(fetch_sales, summarize)
157
+ graph.connect(fetch_churn, summarize)
158
+
159
+ executor = graph.build()
160
+ result = asyncio.run(executor.run(State()))
161
+
162
+ print(result.summary) # Sales up 12% QoQ. Churn down to 4%.
163
+ ```
164
+
165
+ `fetch_sales` and `fetch_churn` both run in the first wave, concurrently. `summarize` waits
166
+ for both before running.
167
+
168
+ **Agent or Graph?** Reach for `as_tool()` when an LLM should decide at runtime whether and
169
+ which sub-agent to call. Reach for `Graph` when the workflow's shape — which steps, which run
170
+ in parallel, in what order — is known ahead of time.
171
+
172
+ See the **[full documentation](https://sayedshaun.github.io/deepharness/)** for graphs, agents,
173
+ tools, providers, and the API reference.
174
+
175
+ ## Development
176
+
177
+ ```bash
178
+ pip install -e ".[dev]"
179
+ pytest -q
180
+ ruff format . && ruff check .
181
+ ```
182
+
183
+ Docs are built with [MkDocs Material](https://squidfunk.github.io/mkdocs-material/):
184
+
185
+ ```bash
186
+ pip install -e ".[docs]"
187
+ mkdocs serve
188
+ ```
189
+
190
+ ---
191
+
192
+ <div align="center">
193
+ <sub>Simple, modular, typed, dependency-light.</sub>
194
+ </div>
@@ -0,0 +1,162 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/assets/logo.svg" width="160" alt="DeepHarness logo">
4
+
5
+ # DeepHarness
6
+
7
+ **Compose LLM agents into typed, concurrent workflows.**
8
+
9
+ A lightweight framework for wiring plain Python functions — and the agents inside them — into a
10
+ graph that runs branches in parallel, routes on conditions, and threads one typed state
11
+ object through the whole thing.
12
+
13
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/) [![Dependencies: httpx only](https://img.shields.io/badge/dependencies-httpx%20only-6E63F5)](pyproject.toml) [![Async native](https://img.shields.io/badge/async-native-0EA5E9)](#quickstart) [![Ruff](https://img.shields.io/badge/lint-ruff-D7FF64?logo=ruff&logoColor=black)](https://github.com/astral-sh/ruff) [![Read the docs](https://img.shields.io/badge/docs-read%20the%20docs-3776AB?logo=materialformkdocs&logoColor=white)](https://sayedshaun.github.io/deepharness/)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## Why DeepHarness
20
+
21
+ - **One dependency. That's it.** `httpx`. Providers talk to vendor REST APIs
22
+ directly — no vendor SDKs, no transitive dependency sprawl.
23
+ - **Your state is a dataclass.** Not an untyped dict. Your editor autocompletes it, your type
24
+ checker checks it.
25
+ - **Nodes are just functions.** Sync or async, decorated in place. No base class to inherit,
26
+ no runner object to construct.
27
+ - **Parallel by default.** Independent branches run concurrently via `asyncio`; sync functions
28
+ are offloaded to threads so a blocking call never stalls the event loop.
29
+ - **One provider interface, a dozen vendors.** `OpenAI`, `Anthropic`, `Gemini`, and OpenAI-compatible
30
+ gateways (`Groq`, `Together`, `Fireworks`, `DeepSeek`, `Mistral`, `Cerebras`, `OpenRouter`, `XAI`,
31
+ `Ollama`, `LMStudio`, `VLLM`) all share the same `LLM` interface.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install deepharness
37
+ ```
38
+
39
+ Or install from source:
40
+
41
+ ```bash
42
+ pip install git+https://github.com/sayedshaun/deepharness.git
43
+ ```
44
+
45
+ ## Quickstart
46
+
47
+ An `Agent` runs a think/act loop against a model: ask for a response, dispatch any tool calls
48
+ it requests, repeat until the model answers with no tool calls.
49
+
50
+ ```python
51
+ from deepharness import Agent, Message, OpenAI, tool
52
+
53
+
54
+ @tool
55
+ def get_weather(city: str) -> str:
56
+ """Look up the current weather for a city."""
57
+ return f"It is 22°C and sunny in {city}."
58
+
59
+
60
+ agent = Agent(
61
+ OpenAI(model="gpt-4o-mini", api_key="sk-..."),
62
+ tools=[get_weather],
63
+ )
64
+
65
+ result = await agent.arun("Weather in Oslo?")
66
+ print(result.output)
67
+ ```
68
+
69
+ An `Agent` can itself be handed to another agent as a tool via `as_tool()`, so one agent can
70
+ delegate a task to another:
71
+
72
+ ```python
73
+ researcher = Agent(OpenAI(model="gpt-4o-mini", api_key="sk-..."), name="researcher")
74
+
75
+ editor = Agent(
76
+ OpenAI(model="gpt-4o-mini", api_key="sk-..."),
77
+ name="editor",
78
+ tools=[researcher.as_tool(description="Look up facts on a topic.")],
79
+ )
80
+ ```
81
+
82
+ ## Graphs
83
+
84
+ For fixed, known-shape workflows — run these steps, some in parallel, then merge — wire plain
85
+ functions (or agents) into a `Graph` instead:
86
+
87
+ ```python
88
+ import asyncio
89
+ from dataclasses import dataclass
90
+
91
+ from deepharness import Graph
92
+
93
+
94
+ @dataclass
95
+ class State:
96
+ sales: str = ""
97
+ churn: str = ""
98
+ summary: str = ""
99
+
100
+
101
+ graph = Graph(State)
102
+
103
+
104
+ @graph.add(start=True)
105
+ async def fetch_sales(state: State) -> State:
106
+ await asyncio.sleep(0.5)
107
+ state.sales = "Sales up 12% QoQ."
108
+ return state
109
+
110
+
111
+ @graph.add(start=True)
112
+ async def fetch_churn(state: State) -> State:
113
+ await asyncio.sleep(0.3)
114
+ state.churn = "Churn down to 4%."
115
+ return state
116
+
117
+
118
+ @graph.add(end=True)
119
+ def summarize(state: State) -> State:
120
+ state.summary = f"{state.sales} {state.churn}"
121
+ return state
122
+
123
+
124
+ graph.connect(fetch_sales, summarize)
125
+ graph.connect(fetch_churn, summarize)
126
+
127
+ executor = graph.build()
128
+ result = asyncio.run(executor.run(State()))
129
+
130
+ print(result.summary) # Sales up 12% QoQ. Churn down to 4%.
131
+ ```
132
+
133
+ `fetch_sales` and `fetch_churn` both run in the first wave, concurrently. `summarize` waits
134
+ for both before running.
135
+
136
+ **Agent or Graph?** Reach for `as_tool()` when an LLM should decide at runtime whether and
137
+ which sub-agent to call. Reach for `Graph` when the workflow's shape — which steps, which run
138
+ in parallel, in what order — is known ahead of time.
139
+
140
+ See the **[full documentation](https://sayedshaun.github.io/deepharness/)** for graphs, agents,
141
+ tools, providers, and the API reference.
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ pip install -e ".[dev]"
147
+ pytest -q
148
+ ruff format . && ruff check .
149
+ ```
150
+
151
+ Docs are built with [MkDocs Material](https://squidfunk.github.io/mkdocs-material/):
152
+
153
+ ```bash
154
+ pip install -e ".[docs]"
155
+ mkdocs serve
156
+ ```
157
+
158
+ ---
159
+
160
+ <div align="center">
161
+ <sub>Simple, modular, typed, dependency-light.</sub>
162
+ </div>
@@ -0,0 +1,101 @@
1
+ from deepharness.agent import (
2
+ FINAL_TOOL,
3
+ Agent,
4
+ AgentState,
5
+ Budget,
6
+ Ctx,
7
+ Finished,
8
+ Message,
9
+ PendingHumanInput,
10
+ StopReason,
11
+ TokenBudgetExceeded,
12
+ Toolbox,
13
+ ToolSpec,
14
+ load_session,
15
+ save_session,
16
+ tool,
17
+ )
18
+ from deepharness.errors import (
19
+ ConfigurationError,
20
+ DeepHarnessError,
21
+ HumanInputRequired,
22
+ ProviderError,
23
+ ToolNotFoundError,
24
+ )
25
+ from deepharness.graph import (
26
+ ConcurrentUpdateError,
27
+ ExecutionError,
28
+ Executor,
29
+ Graph,
30
+ NodeSpec,
31
+ StepLimitExceeded,
32
+ concat,
33
+ merge_dicts,
34
+ )
35
+ from deepharness.providers import (
36
+ VLLM,
37
+ XAI,
38
+ Anthropic,
39
+ Cerebras,
40
+ Completed,
41
+ DeepSeek,
42
+ Fireworks,
43
+ Gemini,
44
+ Groq,
45
+ LMStudio,
46
+ Mistral,
47
+ Ollama,
48
+ OpenAI,
49
+ OpenRouter,
50
+ TextDelta,
51
+ Together,
52
+ TokenUsage,
53
+ )
54
+
55
+ __all__ = [
56
+ "FINAL_TOOL",
57
+ "VLLM",
58
+ "XAI",
59
+ "Agent",
60
+ "AgentState",
61
+ "Anthropic",
62
+ "Budget",
63
+ "Cerebras",
64
+ "Completed",
65
+ "ConcurrentUpdateError",
66
+ "ConfigurationError",
67
+ "Ctx",
68
+ "DeepHarnessError",
69
+ "DeepSeek",
70
+ "ExecutionError",
71
+ "Executor",
72
+ "Finished",
73
+ "Fireworks",
74
+ "Gemini",
75
+ "Graph",
76
+ "Groq",
77
+ "HumanInputRequired",
78
+ "LMStudio",
79
+ "Message",
80
+ "Mistral",
81
+ "NodeSpec",
82
+ "Ollama",
83
+ "OpenAI",
84
+ "OpenRouter",
85
+ "PendingHumanInput",
86
+ "ProviderError",
87
+ "StepLimitExceeded",
88
+ "StopReason",
89
+ "TextDelta",
90
+ "Together",
91
+ "TokenBudgetExceeded",
92
+ "TokenUsage",
93
+ "ToolNotFoundError",
94
+ "ToolSpec",
95
+ "Toolbox",
96
+ "concat",
97
+ "load_session",
98
+ "merge_dicts",
99
+ "save_session",
100
+ "tool",
101
+ ]
@@ -0,0 +1,33 @@
1
+ from ..tools.toolbox import Ctx, Toolbox, ToolSpec, tool
2
+ from .loop import Agent, TokenBudgetExceeded
3
+ from .output import FINAL_TOOL
4
+ from .state import (
5
+ AgentState,
6
+ Budget,
7
+ Finished,
8
+ Message,
9
+ PendingHumanInput,
10
+ StopReason,
11
+ as_dict,
12
+ load_session,
13
+ save_session,
14
+ )
15
+
16
+ __all__ = [
17
+ "FINAL_TOOL",
18
+ "Agent",
19
+ "AgentState",
20
+ "Budget",
21
+ "Ctx",
22
+ "Finished",
23
+ "Message",
24
+ "PendingHumanInput",
25
+ "StopReason",
26
+ "TokenBudgetExceeded",
27
+ "ToolSpec",
28
+ "Toolbox",
29
+ "as_dict",
30
+ "load_session",
31
+ "save_session",
32
+ "tool",
33
+ ]