Skip to content

Commit ece639e

Browse files
authored
Merge branch 'main' into sc-27873-get-prompt-template-output
2 parents 44c692f + a94ed8c commit ece639e

File tree

10 files changed

+256
-2
lines changed

10 files changed

+256
-2
lines changed

examples/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## Collection of Python galileo examples
2+
3+
### Preconditions
4+
5+
Install `uv`, we use inline dependency inside scripts.
6+
7+
### How to use/run?
8+
9+
First of all create `.env` file and add required env vars based on `.env.sample`.
10+
11+
Then just run `uv`:
12+
13+
```bash
14+
uv run --env-file=examples/langgraph/.env examples/langgraph/with_openai.py
15+
```
16+
17+
or
18+
19+
#### [basic_langgraph.py]
20+
```bash
21+
uv run --env-file=examples/langgraph/.env examples/langgraph/basic_langgraph.py
22+
```

examples/langgraph/.env.sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
GALILEO_API_KEY=
2+
GALILEO_PROJECT=
3+
GALILEO_LOG_STREAM=
4+
OPENAI_API_KEY=

examples/langgraph/basic_langgraph.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = [
4+
# "galileo",
5+
# "langgraph",
6+
# "langsmith",
7+
# "langchain",
8+
# "grandalf", # for printing graph in ascii
9+
# ]
10+
# ///
11+
# from dotenv import load_dotenv; load_dotenv()
12+
13+
from typing import Annotated
14+
15+
from langchain_core.messages import AIMessage
16+
from langgraph.graph import END, START, StateGraph
17+
from langgraph.graph.message import add_messages
18+
from typing_extensions import TypedDict
19+
20+
from galileo.handlers.langchain import GalileoCallback
21+
22+
23+
class State(TypedDict):
24+
# Messages have the type "list". The `add_messages` function
25+
# in the annotation defines how this state key should be updated
26+
# (in this case, it appends messages to the list, rather than overwriting them)
27+
messages: Annotated[list, add_messages]
28+
29+
30+
def node(state: State):
31+
messages = state["messages"]
32+
new_message = AIMessage("Hello!")
33+
34+
return {"messages": messages + [new_message], "extra_field": 10}
35+
36+
37+
def node2(state: State):
38+
return {"messages": state["messages"]}
39+
40+
41+
graph_builder = StateGraph(State)
42+
graph_builder.add_node("node_name", node)
43+
graph_builder.add_edge(START, "node_name")
44+
graph_builder.add_edge("node_name", END)
45+
graph = graph_builder.compile()
46+
47+
graph.get_graph().print_ascii()
48+
graph.invoke({"messages": [{"role": "user", "content": "hi!"}]}, config={"callbacks": [GalileoCallback()]})

examples/langgraph/with_openai.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = [
4+
# "galileo",
5+
# "langgraph",
6+
# "langsmith",
7+
# "langchain[openai]",
8+
# "grandalf", # for printing graph in ascii
9+
# ]
10+
# ///
11+
from typing import Annotated
12+
13+
from langchain_openai import ChatOpenAI
14+
from langgraph.graph import START, StateGraph
15+
from langgraph.graph.message import add_messages
16+
from typing_extensions import TypedDict
17+
18+
from galileo.handlers.langchain import GalileoCallback
19+
20+
21+
class State(TypedDict):
22+
# Messages have the type "list". The `add_messages` function
23+
# in the annotation defines how this state key should be updated
24+
# (in this case, it appends messages to the list, rather than overwriting them)
25+
messages: Annotated[list, add_messages]
26+
27+
28+
llm = ChatOpenAI(model="gpt-4")
29+
30+
31+
def chatbot(state: State):
32+
return {"messages": [llm.invoke(state["messages"])]}
33+
34+
35+
graph_builder = StateGraph(State)
36+
# The first argument is the unique node name
37+
# The second argument is the function or object that will be called whenever
38+
# the node is used.
39+
graph_builder.add_node("chatbot", chatbot)
40+
graph_builder.add_edge(START, "chatbot")
41+
graph = graph_builder.compile()
42+
43+
graph.get_graph().print_ascii()
44+
45+
graph.invoke({"messages": [{"role": "user", "content": "hi!"}]}, {"callbacks": [GalileoCallback()]})

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "galileo"
3-
version = "0.10.0"
3+
version = "1.0.0"
44
description = "Client library for the Galileo platform."
55
authors = [{ name = "Galileo Technologies Inc.", email = "team@galileo.ai" }]
66
readme = "README.md"

src/galileo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
from galileo_core.schemas.logging.step import StepType
1414
from galileo_core.schemas.logging.trace import Trace
1515

16-
__version__ = "0.10.0"
16+
__version__ = "1.0.0"

src/galileo/handlers/langchain/async_handler.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import logging
33
import time
4+
from datetime import datetime, timezone
45
from typing import Any, Optional
56
from uuid import UUID
67

@@ -109,6 +110,7 @@ async def _log_node_tree(self, node: Node) -> None:
109110
name = node.span_params.get("name")
110111
metadata = node.span_params.get("metadata")
111112
tags = node.span_params.get("tags")
113+
created_at = node.span_params.get("created_at")
112114

113115
# Convert metadata to a dict[str, str]
114116
if metadata is not None:
@@ -123,6 +125,7 @@ async def _log_node_tree(self, node: Node) -> None:
123125
duration_ns=node.span_params.get("duration_ns"),
124126
metadata=metadata,
125127
tags=tags,
128+
created_at=created_at,
126129
)
127130
is_workflow_span = True
128131
elif node.node_type in ("llm", "chat"):
@@ -140,6 +143,7 @@ async def _log_node_tree(self, node: Node) -> None:
140143
num_output_tokens=node.span_params.get("num_output_tokens"),
141144
total_tokens=node.span_params.get("total_tokens"),
142145
time_to_first_token_ns=node.span_params.get("time_to_first_token_ns"),
146+
created_at=created_at,
143147
)
144148
elif node.node_type == "retriever":
145149
self._galileo_logger.add_retriever_span(
@@ -149,6 +153,7 @@ async def _log_node_tree(self, node: Node) -> None:
149153
duration_ns=node.span_params.get("duration_ns"),
150154
metadata=metadata,
151155
tags=tags,
156+
created_at=created_at,
152157
)
153158
elif node.node_type == "tool":
154159
self._galileo_logger.add_tool_span(
@@ -158,6 +163,7 @@ async def _log_node_tree(self, node: Node) -> None:
158163
duration_ns=node.span_params.get("duration_ns"),
159164
metadata=metadata,
160165
tags=tags,
166+
created_at=created_at,
161167
)
162168
else:
163169
_logger.warning(f"Unknown node type: {node.node_type}")
@@ -208,9 +214,13 @@ async def _start_node(
208214
# Create new node
209215
node = Node(node_type=node_type, span_params=kwargs, run_id=run_id, parent_run_id=parent_run_id)
210216

217+
# start_time is used to calculate duration_ns
211218
if "start_time" not in node.span_params:
212219
node.span_params["start_time"] = time.perf_counter_ns()
213220

221+
if "created_at" not in node.span_params:
222+
node.span_params["created_at"] = datetime.now(tz=timezone.utc)
223+
214224
self._nodes[node_id] = node
215225

216226
# Set as root node if needed

src/galileo/handlers/langchain/handler.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import logging
44
import time
5+
from datetime import datetime, timezone
56
from typing import Any, Optional
67
from uuid import UUID
78

@@ -111,6 +112,7 @@ def _log_node_tree(self, node: Node) -> None:
111112
name = node.span_params.get("name")
112113
metadata = node.span_params.get("metadata")
113114
tags = node.span_params.get("tags")
115+
created_at = node.span_params.get("created_at")
114116

115117
# Convert metadata to a dict[str, str]
116118
if metadata is not None:
@@ -125,6 +127,7 @@ def _log_node_tree(self, node: Node) -> None:
125127
duration_ns=node.span_params.get("duration_ns"),
126128
metadata=metadata,
127129
tags=tags,
130+
created_at=created_at,
128131
)
129132
is_workflow_span = True
130133
elif node.node_type in ("llm", "chat"):
@@ -142,6 +145,7 @@ def _log_node_tree(self, node: Node) -> None:
142145
num_output_tokens=node.span_params.get("num_output_tokens"),
143146
total_tokens=node.span_params.get("total_tokens"),
144147
time_to_first_token_ns=node.span_params.get("time_to_first_token_ns"),
148+
created_at=created_at,
145149
)
146150
elif node.node_type == "retriever":
147151
self._galileo_logger.add_retriever_span(
@@ -151,6 +155,7 @@ def _log_node_tree(self, node: Node) -> None:
151155
duration_ns=node.span_params.get("duration_ns"),
152156
metadata=metadata,
153157
tags=tags,
158+
created_at=created_at,
154159
)
155160
elif node.node_type == "tool":
156161
self._galileo_logger.add_tool_span(
@@ -160,6 +165,7 @@ def _log_node_tree(self, node: Node) -> None:
160165
duration_ns=node.span_params.get("duration_ns"),
161166
metadata=metadata,
162167
tags=tags,
168+
created_at=created_at,
163169
)
164170
else:
165171
_logger.warning(f"Unknown node type: {node.node_type}")
@@ -210,9 +216,13 @@ def _start_node(
210216
# Create new node
211217
node = Node(node_type=node_type, span_params=kwargs, run_id=run_id, parent_run_id=parent_run_id)
212218

219+
# start_time is used to calculate duration_ns
213220
if "start_time" not in node.span_params:
214221
node.span_params["start_time"] = time.perf_counter_ns()
215222

223+
if "created_at" not in node.span_params:
224+
node.span_params["created_at"] = datetime.now(tz=timezone.utc)
225+
216226
self._nodes[node_id] = node
217227

218228
# Set as root node if needed

tests/test_langchain.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import time
12
import uuid
23
from unittest.mock import MagicMock, Mock, patch
34

@@ -690,3 +691,59 @@ def test_callback_with_active_trace(self, galileo_logger: GalileoLogger):
690691
assert traces[0].spans[0].spans[0].type == "retriever"
691692
assert traces[0].spans[0].spans[0].input == "test query"
692693
assert traces[0].spans[0].spans[0].output == [GalileoDocument(content="test document", metadata={})]
694+
695+
def test_node_created_at(self, callback: GalileoCallback, galileo_logger: GalileoLogger):
696+
parent_id = uuid.uuid4()
697+
llm_run_id = uuid.uuid4()
698+
retriever_run_id = uuid.uuid4()
699+
700+
# Create parent chain
701+
callback.on_chain_start(serialized={}, inputs={"query": "test"}, run_id=parent_id)
702+
703+
# Start retriever
704+
callback.on_retriever_start(
705+
serialized={}, query="AI development", run_id=retriever_run_id, parent_run_id=parent_id
706+
)
707+
708+
# End retriever
709+
document = Document(page_content="AI is advancing rapidly", metadata={"source": "textbook"})
710+
callback.on_retriever_end(documents=[document], run_id=retriever_run_id, parent_run_id=parent_id)
711+
712+
delay_ms = 500
713+
714+
time.sleep(delay_ms / 1000)
715+
716+
callback.on_llm_start(
717+
serialized={},
718+
prompts=["Tell me about AI"],
719+
run_id=llm_run_id,
720+
parent_run_id=parent_id,
721+
invocation_params={"model_name": "gpt-4", "temperature": 0.7},
722+
)
723+
724+
# Add a token to test token timing
725+
callback.on_llm_new_token("AI", run_id=llm_run_id)
726+
727+
# End LLM
728+
llm_response = MagicMock()
729+
llm_response.generations = [[MagicMock()]]
730+
llm_response.llm_output = {"token_usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30}}
731+
732+
# Mock dict method on the generation
733+
llm_response.generations[0][0].dict.return_value = {"text": "AI is a technology..."}
734+
735+
callback.on_llm_end(response=llm_response, run_id=llm_run_id, parent_run_id=parent_id)
736+
737+
# End chain
738+
callback.on_chain_end(outputs='{"result": "test answer"}', run_id=parent_id)
739+
740+
traces = galileo_logger.traces
741+
assert len(traces) == 1
742+
assert len(traces[0].spans) == 1
743+
assert len(traces[0].spans[0].spans) == 2
744+
745+
retriever_span = traces[0].spans[0].spans[0]
746+
llm_span = traces[0].spans[0].spans[1]
747+
748+
time_diff_ms = (llm_span.created_at - retriever_span.created_at).total_seconds() * 1000
749+
assert time_diff_ms >= delay_ms

tests/test_langchain_async.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import uuid
23
from unittest.mock import MagicMock, Mock, patch
34

@@ -667,3 +668,60 @@ async def test_callback_with_active_trace(self, galileo_logger: GalileoLogger):
667668
assert traces[0].spans[0].spans[0].type == "retriever"
668669
assert traces[0].spans[0].spans[0].input == "test query"
669670
assert traces[0].spans[0].spans[0].output == [GalileoDocument(content="test document", metadata={})]
671+
672+
@mark.asyncio
673+
async def test_node_created_at(self, callback: GalileoAsyncCallback, galileo_logger: GalileoLogger):
674+
parent_id = uuid.uuid4()
675+
llm_run_id = uuid.uuid4()
676+
retriever_run_id = uuid.uuid4()
677+
678+
# Create parent chain
679+
await callback.on_chain_start(serialized={}, inputs={"query": "test"}, run_id=parent_id)
680+
681+
# Start retriever
682+
await callback.on_retriever_start(
683+
serialized={}, query="AI development", run_id=retriever_run_id, parent_run_id=parent_id
684+
)
685+
686+
# End retriever
687+
document = Document(page_content="AI is advancing rapidly", metadata={"source": "textbook"})
688+
await callback.on_retriever_end(documents=[document], run_id=retriever_run_id, parent_run_id=parent_id)
689+
690+
delay_ms = 500
691+
692+
await asyncio.sleep(delay_ms / 1000)
693+
694+
await callback.on_llm_start(
695+
serialized={},
696+
prompts=["Tell me about AI"],
697+
run_id=llm_run_id,
698+
parent_run_id=parent_id,
699+
invocation_params={"model_name": "gpt-4", "temperature": 0.7},
700+
)
701+
702+
# Add a token to test token timing
703+
await callback.on_llm_new_token("AI", run_id=llm_run_id)
704+
705+
# End LLM
706+
llm_response = MagicMock()
707+
llm_response.generations = [[MagicMock()]]
708+
llm_response.llm_output = {"token_usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30}}
709+
710+
# Mock dict method on the generation
711+
llm_response.generations[0][0].dict.return_value = {"text": "AI is a technology..."}
712+
713+
await callback.on_llm_end(response=llm_response, run_id=llm_run_id, parent_run_id=parent_id)
714+
715+
# End chain
716+
await callback.on_chain_end(outputs='{"result": "test answer"}', run_id=parent_id)
717+
718+
traces = galileo_logger.traces
719+
assert len(traces) == 1
720+
assert len(traces[0].spans) == 1
721+
assert len(traces[0].spans[0].spans) == 2
722+
723+
retriever_span = traces[0].spans[0].spans[0]
724+
llm_span = traces[0].spans[0].spans[1]
725+
726+
time_diff_ms = (llm_span.created_at - retriever_span.created_at).total_seconds() * 1000
727+
assert time_diff_ms >= delay_ms

0 commit comments

Comments
 (0)