Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.

Commit a13de56

Browse files
authored
Add langgraph observability and layers (#6)
* add langchain callback * implement layers client and nodes * ignore non-code paths on workflow triggers; move layers module --------- Co-authored-by: akhatre <akhatre@users.noreply.github.com>
1 parent 267b197 commit a13de56

16 files changed

Lines changed: 1253 additions & 59 deletions

File tree

.github/workflows/pr-checks.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ on:
55
branches:
66
- main
77
types: [opened, synchronize, reopened]
8+
paths-ignore:
9+
- 'docs/**'
10+
- '*.md'
11+
- 'README.md'
812

913
jobs:
1014
test_and_check_version:

.github/workflows/publish.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ on:
44
push:
55
branches:
66
- main
7+
paths-ignore:
8+
- 'docs/**'
9+
- '*.md'
10+
- 'README.md'
711

812
jobs:
913
deploy:

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@
33
.PHONY: test-publish
44
test-publish:
55
poetry build
6-
twine upload --repository testpypi dist/* --
6+
twine upload --repository testpypi dist/* --
7+
8+
9+
.PHONY: format
10+
format:
11+
poetry run ruff format

overmind/agents.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def create(
2929
) -> Dict[str, str]:
3030
"""
3131
Create a new agent.
32-
32+
3333
Args:
3434
agent_id: Unique identifier for the agent
3535
agent_model: The AI model to use (e.g., 'gpt-4o')
@@ -71,7 +71,7 @@ def list(self) -> List[AgentResponse]:
7171
def get(self, agent_id: str) -> AgentResponse:
7272
"""
7373
Get a specific agent by ID.
74-
74+
7575
Args:
7676
agent_id: The unique identifier of the agent to retrieve
7777
"""
@@ -92,7 +92,7 @@ def update(
9292
) -> Dict[str, str]:
9393
"""
9494
Update an existing agent.
95-
95+
9696
Args:
9797
agent_id: Unique identifier for the agent
9898
agent_model: The AI model to use (e.g., 'gpt-4o')
@@ -128,7 +128,7 @@ def update(
128128
def delete(self, agent_id: str) -> Dict[str, str]:
129129
"""
130130
Delete an agent by ID.
131-
131+
132132
Args:
133133
agent_id: The unique identifier of the agent to delete
134134
"""

overmind/client.py

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
"""
44

55
import os
6-
from typing import Any, Dict, List, Optional, Union
6+
from functools import lru_cache
7+
from typing import Any, Dict, List, Optional, Sequence, Union
78
from urllib.parse import urljoin
89

910
import requests
1011

1112
from .exceptions import OvermindAPIError, OvermindAuthenticationError, OvermindError
12-
from .models import InvocationResponse
13+
from .models import InvocationResponse, LayerResponse
1314
from .agents import AgentsClient
1415
from .policies import PoliciesClient
1516
from .invocations import InvocationsClient
16-
17+
from .utils.api_settings import get_api_settings
1718

1819
# Mapping of common environment variables to provider parameter names
1920
COMMON_ENV_VARS = {
@@ -96,24 +97,9 @@ def __init__(
9697
Raises:
9798
OvermindError: If no API key is provided and OVERMIND_API_KEY environment variable is not set
9899
"""
99-
# Get API key from parameter or environment variable
100-
if overmind_api_key is None:
101-
overmind_api_key = os.getenv("OVERMIND_API_KEY")
102-
if overmind_api_key is None:
103-
raise OvermindError(
104-
"No Overmind API key provided. Either pass 'overmind_api_key' parameter "
105-
"or set OVERMIND_API_KEY environment variable."
106-
)
107-
108-
if base_url is None:
109-
base_url = os.getenv("OVERMIND_API_URL")
110-
if base_url is None:
111-
base_url = (
112-
"https://api.evallab.dev"
113-
)
114-
115-
self.overmind_api_key = overmind_api_key
116-
self.base_url = base_url.rstrip("/")
100+
self.overmind_api_key, self.base_url, self.traces_base_url = get_api_settings(
101+
overmind_api_key, base_url, None
102+
)
117103

118104
# Start with provided provider parameters
119105
self.provider_parameters = (
@@ -129,7 +115,7 @@ def __init__(
129115
self.session = requests.Session()
130116
self.session.headers.update(
131117
{
132-
"Authorization": f"Bearer {overmind_api_key}",
118+
"Authorization": f"Bearer {self.overmind_api_key}",
133119
"Content-Type": "application/json",
134120
}
135121
)
@@ -237,3 +223,55 @@ def invoke(
237223
)
238224

239225
return InvocationResponse(**response_data)
226+
227+
228+
class OvermindLayersClient:
229+
def __init__(
230+
self,
231+
overmind_api_key: Optional[str] = None,
232+
base_url: Optional[str] = None,
233+
traces_base_url: Optional[str] = None,
234+
):
235+
self.overmind_api_key, self.base_url, self.traces_base_url = get_api_settings(
236+
overmind_api_key, base_url, traces_base_url
237+
)
238+
self.session = requests.Session()
239+
self.session.headers.update(
240+
{
241+
"Authorization": f"Bearer {self.overmind_api_key}",
242+
"Content-Type": "application/json",
243+
}
244+
)
245+
246+
def run_layer(
247+
self, input_data: str, policies: Sequence[str | dict]
248+
) -> LayerResponse:
249+
"""
250+
Run a layer of the Overmind API.
251+
"""
252+
payload = {
253+
"input_data": input_data,
254+
"policies": policies,
255+
}
256+
257+
response_data = self.session.request(
258+
"POST", f"{self.base_url}/api/v1/layers/run", json=payload
259+
)
260+
261+
if response_data.status_code != 200:
262+
raise OvermindAPIError(
263+
message=response_data.text,
264+
status_code=response_data.status_code,
265+
response_data=response_data.json(),
266+
)
267+
268+
return LayerResponse(**response_data.json())
269+
270+
271+
@lru_cache
272+
def get_layers_client(
273+
overmind_api_key: Optional[str] = None,
274+
base_url: Optional[str] = None,
275+
traces_base_url: Optional[str] = None,
276+
):
277+
return OvermindLayersClient(overmind_api_key, base_url, traces_base_url)

overmind/formatters.py

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,55 +10,75 @@ def summarise_invocation(result) -> None:
1010
# -- 2. Set up Rich console and formatting maps --
1111
console = Console()
1212
res_map = {
13-
'passed': {'color': 'green', 'emoji': '✅'},
14-
'altered': {'color': 'yellow', 'emoji': '⚠️'},
15-
'rejected': {'color': 'red', 'emoji': '❌'},
13+
"passed": {"color": "green", "emoji": "✅"},
14+
"altered": {"color": "yellow", "emoji": "⚠️"},
15+
"rejected": {"color": "red", "emoji": "❌"},
1616
}
1717

18-
1918
# -- 3. Build the Rich renderable components --
2019

2120
# A Table for Invocation Results
22-
inv_table = Table(show_header=True, header_style="bold white", title="Invocation Results")
21+
inv_table = Table(
22+
show_header=True, header_style="bold white", title="Invocation Results"
23+
)
2324
inv_table.add_column("Component", style="dim")
2425
inv_table.add_column("Outcome", justify="center")
2526

2627
for part, outcome_str in result.invocation_results.items():
27-
style = res_map.get(outcome_str, {}).get('color', 'white')
28-
emoji = res_map.get(outcome_str, {}).get('emoji', '➡️')
28+
style = res_map.get(outcome_str, {}).get("color", "white")
29+
emoji = res_map.get(outcome_str, {}).get("emoji", "➡️")
2930
outcome_text = Text(f"{emoji} {outcome_str.upper()}", style=style)
3031
inv_table.add_row(part.replace("_", " ").capitalize(), outcome_text)
3132

3233
# A Table for Policy Results
33-
pol_table = Table(show_header=True, header_style="bold white", title="\nPolicy Results")
34+
pol_table = Table(
35+
show_header=True, header_style="bold white", title="\nPolicy Results"
36+
)
3437
pol_table.add_column("Policy", style="dim", width=30)
3538
pol_table.add_column("Details", width=70)
3639

3740
for policy, outcome in result.policy_results.items():
3841
# Assuming the outcome is a dict, format it nicely with Syntax
3942
outcome_str = json.dumps(outcome, indent=2)
40-
outcome_syntax = Syntax(outcome_str, "json", theme="solarized-dark", word_wrap=True)
43+
outcome_syntax = Syntax(
44+
outcome_str, "json", theme="solarized-dark", word_wrap=True
45+
)
4146
pol_table.add_row(policy, outcome_syntax)
4247

4348
# Syntax Panels for Processed Input and Output
44-
input_color = res_map.get(result.invocation_results['input'], {}).get('color', 'white')
45-
output_color = res_map.get(result.invocation_results.get('output'), {}).get('color', 'white')
46-
49+
input_color = res_map.get(result.invocation_results["input"], {}).get(
50+
"color", "white"
51+
)
52+
output_color = res_map.get(result.invocation_results.get("output"), {}).get(
53+
"color", "white"
54+
)
55+
4756
input_panel = Panel(
48-
Syntax(result.processed_input, "plain", theme="solarized-dark", line_numbers=False, word_wrap=True),
57+
Syntax(
58+
result.processed_input,
59+
"plain",
60+
theme="solarized-dark",
61+
line_numbers=False,
62+
word_wrap=True,
63+
),
4964
title="[bold]Processed Input[/bold]",
50-
border_style=input_color, # The border color reflects the outcome!
65+
border_style=input_color, # The border color reflects the outcome!
5166
title_align="left",
5267
)
5368

5469
output_panel = Panel(
55-
Syntax((result.processed_output or result.raw_output)[:2000], "plain", theme="solarized-dark", line_numbers=False, word_wrap=True),
70+
Syntax(
71+
(result.processed_output or result.raw_output)[:2000],
72+
"plain",
73+
theme="solarized-dark",
74+
line_numbers=False,
75+
word_wrap=True,
76+
),
5677
title="[bold]Processed Output[/bold]",
57-
border_style=output_color, # The border color reflects the outcome!
78+
border_style=output_color, # The border color reflects the outcome!
5879
title_align="left",
5980
)
6081

61-
6282
# -- 4. Group components and print inside a final Panel --
6383
final_group = Group(
6484
inv_table,
@@ -71,6 +91,6 @@ def summarise_invocation(result) -> None:
7191
Panel(
7292
final_group,
7393
title="[bold cyan]Invocation & Policy Summary[/bold cyan]",
74-
border_style="green"
94+
border_style="green",
7595
)
76-
)
96+
)

overmind/langchain/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)