-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(max): convert the insights graph to a Max tool #39893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
47bb37d
c077c0d
bbca0c6
abc2c56
2a099d8
d0ce76d
43cc476
8d78599
0f3499f
4ac1438
ab09177
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from .graph import AssistantCompiledStateGraph, BaseAssistantGraph, global_checkpointer | ||
| from .node import AssistantNode, BaseAssistantNode | ||
|
|
||
| __all__ = [ | ||
| "BaseAssistantNode", | ||
| "AssistantNode", | ||
| "BaseAssistantGraph", | ||
| "AssistantCompiledStateGraph", | ||
| "global_checkpointer", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| from collections.abc import Callable, Coroutine | ||
| from typing import Any, Generic, Literal, Protocol, runtime_checkable | ||
|
|
||
| from langgraph.graph.state import CompiledStateGraph, StateGraph | ||
|
|
||
| from posthog.schema import ReasoningMessage | ||
|
|
||
| from posthog.models import Team, User | ||
|
|
||
| from ee.hogai.django_checkpoint.checkpointer import DjangoCheckpointer | ||
| from ee.hogai.utils.types import AssistantNodeName, StateType | ||
| from ee.hogai.utils.types.base import BaseState | ||
| from ee.hogai.utils.types.composed import MaxNodeName | ||
|
|
||
| # Base checkpointer for all graphs | ||
| global_checkpointer = DjangoCheckpointer() | ||
|
|
||
|
|
||
| # Type alias for async reasoning message function, takes a state and an optional default message content and returns an optional reasoning message | ||
| GetReasoningMessageAfunc = Callable[[BaseState, str | None], Coroutine[Any, Any, ReasoningMessage | None]] | ||
| GetReasoningMessageMapType = dict[MaxNodeName, GetReasoningMessageAfunc] | ||
|
|
||
|
|
||
| # Protocol to check if a node has a reasoning message function at runtime | ||
| @runtime_checkable | ||
| class HasReasoningMessage(Protocol): | ||
| get_reasoning_message: GetReasoningMessageAfunc | ||
|
|
||
|
|
||
| class AssistantCompiledStateGraph(CompiledStateGraph): | ||
| """Wrapper around CompiledStateGraph that preserves reasoning message information. | ||
| Note: This uses __dict__ copying as a workaround since CompiledStateGraph | ||
| doesn't support standard inheritance. This is brittle and may break with | ||
| library updates. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, compiled_graph: CompiledStateGraph, aget_reasoning_message_by_node_name: GetReasoningMessageMapType | ||
| ): | ||
| # Copy the internal state from the compiled graph without calling super().__init__ | ||
| # This is a workaround since CompiledStateGraph doesn't support standard inheritance | ||
| self.__dict__.update(compiled_graph.__dict__) | ||
| self.aget_reasoning_message_by_node_name = aget_reasoning_message_by_node_name | ||
|
|
||
|
|
||
| class BaseAssistantGraph(Generic[StateType]): | ||
| _team: Team | ||
| _user: User | ||
| _graph: StateGraph | ||
| aget_reasoning_message_by_node_name: GetReasoningMessageMapType | ||
|
|
||
| def __init__(self, team: Team, user: User, state_type: type[StateType]): | ||
| self._team = team | ||
| self._user = user | ||
| self._graph = StateGraph(state_type) | ||
| self._has_start_node = False | ||
| self.aget_reasoning_message_by_node_name = {} | ||
|
|
||
| def add_edge(self, from_node: MaxNodeName, to_node: MaxNodeName): | ||
| if from_node == AssistantNodeName.START: | ||
| self._has_start_node = True | ||
| self._graph.add_edge(from_node, to_node) | ||
| return self | ||
|
|
||
| def add_node(self, node: MaxNodeName, action: Any): | ||
| self._graph.add_node(node, action) | ||
| if isinstance(action, HasReasoningMessage): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I remembered that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't know about that. ChatGPT confirms it, but it's still nanoseconds, so it's okay for a single check. I don't see a reason not to use it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have a great memory so maybe ChatGPT is better lol, but I think it was related to accessing |
||
| self.aget_reasoning_message_by_node_name[node] = action.get_reasoning_message | ||
| return self | ||
|
|
||
| def add_subgraph(self, node_name: MaxNodeName, subgraph: AssistantCompiledStateGraph): | ||
| self._graph.add_node(node_name, subgraph) | ||
| self.aget_reasoning_message_by_node_name.update(subgraph.aget_reasoning_message_by_node_name) | ||
| return self | ||
|
|
||
| def compile(self, checkpointer: DjangoCheckpointer | None | Literal[False] = None): | ||
| if not self._has_start_node: | ||
| raise ValueError("Start node not added to the graph") | ||
| # TRICKY: We check `is not None` because False has a special meaning of "no checkpointer", which we want to pass on | ||
| compiled_graph = self._graph.compile( | ||
| checkpointer=checkpointer if checkpointer is not None else global_checkpointer | ||
| ) | ||
| return AssistantCompiledStateGraph( | ||
| compiled_graph, aget_reasoning_message_by_node_name=self.aget_reasoning_message_by_node_name | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI,
AssistantCompiledStateGraphis gone on myloop-executor-ui-fullbranch, so there will be conflicts (I can fix them once we get there)