feat(max): expose max context from scene logics (#34173)
Co-authored-by: kappa90 <e.capparelli@gmail.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Michael Matloka <michael@matloka.com> Co-authored-by: Michael Matloka <dev@twixes.com>
52
cli/Cargo.lock
generated
@@ -196,9 +196,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
version = "3.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -573,12 +573,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.13"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -894,7 +894,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.1",
|
||||
"webpki-roots 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1066,9 +1066,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.10.0"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
@@ -1188,9 +1188,9 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.4"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
@@ -1421,9 +1421,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.2.2"
|
||||
version = "4.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
|
||||
checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
@@ -1486,7 +1486,7 @@ dependencies = [
|
||||
"posthog-rs",
|
||||
"posthog-symbol-data",
|
||||
"ratatui",
|
||||
"reqwest 0.12.22",
|
||||
"reqwest 0.12.20",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1595,9 +1595,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.13"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
|
||||
checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -1785,9 +1785,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.22"
|
||||
version = "0.12.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
|
||||
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -1821,7 +1821,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 1.0.1",
|
||||
"webpki-roots 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2195,9 +2195,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
version = "2.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2269,9 +2269,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "test-log"
|
||||
version = "0.2.18"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e33b98a582ea0be1168eba097538ee8dd4bbe0f2b01b22ac92ea30054e5be7b"
|
||||
checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"test-log-macros",
|
||||
@@ -2280,9 +2280,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "test-log-macros"
|
||||
version = "0.2.18"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36"
|
||||
checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2803,9 +2803,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.1"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502"
|
||||
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
@@ -110,3 +110,86 @@ If you've got any requests for Max, including around tools, let us know at #team
|
||||
- Allow users to both get things done from scratch, and refine what's already there
|
||||
|
||||
For a _lot_ of great detail on prompting, check out the [GPT-4.1 prompting guide](https://cookbook.openai.com/examples/gpt4-1_prompting_guide). While somewhat GPT-4.1 specific, those principles largely apply to LLMs overall.
|
||||
|
||||
## Support new query types
|
||||
|
||||
Max can now read from frontend context multiple query types like trends, funnels, retention, and HogQL queries. To add support for new query types, you need to extend both the QueryExecutor and the Root node.
|
||||
|
||||
NOTE: this won't extend query types generation. For that, talk to the Max AI team.
|
||||
|
||||
### Adding a new query type
|
||||
|
||||
1. **Update the query executor** (`@ee/hogai/graph/query_executor/`):
|
||||
|
||||
- Add your new query type to the `SupportedQueryTypes` union in `query_executor.py:33`:
|
||||
```python
|
||||
SupportedQueryTypes = (
|
||||
AssistantTrendsQuery
|
||||
| TrendsQuery
|
||||
| AssistantFunnelsQuery
|
||||
| FunnelsQuery
|
||||
| AssistantRetentionQuery
|
||||
| RetentionQuery
|
||||
| AssistantHogQLQuery
|
||||
| HogQLQuery
|
||||
| YourNewQuery # Add your query type
|
||||
)
|
||||
```
|
||||
|
||||
- Add a new formatter class in `query_executor/format.py` that implements query result formatting for AI consumption (see below, point 3)
|
||||
- Add formatting logic to `_compress_results()` method in `query_executor/query_executor.py`:
|
||||
```python
|
||||
elif isinstance(query, YourNewAssistantQuery | YourNewQuery):
|
||||
return YourNewResultsFormatter(query, response["results"]).format()
|
||||
```
|
||||
- Add example prompts for your query type in `query_executor/prompts.py`, this explains to the LLM the query results formatting
|
||||
- Update `_get_example_prompt()` method in `query_executor/nodes.py` to handle your new query type:
|
||||
```python
|
||||
if isinstance(viz_message.answer, YourNewAssistantQuery):
|
||||
return YOUR_NEW_EXAMPLE_PROMPT
|
||||
```
|
||||
|
||||
2. **Update the root node** (`@ee/hogai/graph/root/`):
|
||||
|
||||
- Add your new query type to the `MAX_SUPPORTED_QUERY_KIND_TO_MODEL` mapping in `nodes.py:57`:
|
||||
```python
|
||||
MAX_SUPPORTED_QUERY_KIND_TO_MODEL: dict[str, type[SupportedQueryTypes]] = {
|
||||
"TrendsQuery": TrendsQuery,
|
||||
"FunnelsQuery": FunnelsQuery,
|
||||
"RetentionQuery": RetentionQuery,
|
||||
"HogQLQuery": HogQLQuery,
|
||||
"YourNewQuery": YourNewQuery, # Add your query mapping
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create the formatter class**:
|
||||
|
||||
Create a new formatter in `format.py` following the pattern of existing formatters:
|
||||
```python
|
||||
class YourNewResultsFormatter:
|
||||
def __init__(self, query: YourNewQuery, results: dict, team: Optional[Team] = None, utc_now_datetime: Optional[datetime] = None):
|
||||
self._query = query
|
||||
self._results = results
|
||||
self._team = team
|
||||
self._utc_now_datetime = utc_now_datetime
|
||||
|
||||
def format(self) -> str:
|
||||
# Format your query results for AI consumption
|
||||
# Return a string representation optimized for LLM understanding
|
||||
pass
|
||||
```
|
||||
|
||||
4. **Add tests**:
|
||||
- Add test cases in `test/test_query_executor.py` for your new query type
|
||||
- Add test cases in `test/test_format.py` for your new formatter
|
||||
- Ensure tests cover both successful execution and error handling
|
||||
|
||||
### Key considerations
|
||||
|
||||
- **Query execution**: The `AssistantQueryExecutor` class handles the complete query lifecycle including async polling and error handling
|
||||
- **Result formatting**: Each query type needs a specialized formatter that converts raw results into AI-readable format
|
||||
- **Error handling**: The system provides fallback to raw JSON if custom formatting fails
|
||||
- **Context awareness**: The root node provides UI context (dashboards, insights, events, actions) to help the AI understand the current state
|
||||
- **Memory integration**: The system can access core memory and onboarding state to provide contextual responses
|
||||
|
||||
The query executor is designed to be extensible while maintaining robustness through comprehensive error handling and fallback mechanisms.
|
||||
|
||||
@@ -12,7 +12,7 @@ from posthog.schema import (
|
||||
AssistantToolCall,
|
||||
HumanMessage,
|
||||
MaxActionContext,
|
||||
MaxContextShape,
|
||||
MaxUIContext,
|
||||
MaxEventContext,
|
||||
)
|
||||
|
||||
@@ -78,7 +78,7 @@ async def eval_ui_context_actions(call_root_with_ui_context, sample_action):
|
||||
EvalCase(
|
||||
input={
|
||||
"messages": "Show me trends for this action",
|
||||
"ui_context": MaxContextShape(
|
||||
"ui_context": MaxUIContext(
|
||||
actions=[
|
||||
MaxActionContext(
|
||||
id=sample_action.id,
|
||||
@@ -101,7 +101,7 @@ async def eval_ui_context_actions(call_root_with_ui_context, sample_action):
|
||||
EvalCase(
|
||||
input={
|
||||
"messages": "Create a funnel using these actions",
|
||||
"ui_context": MaxContextShape(
|
||||
"ui_context": MaxUIContext(
|
||||
actions=[
|
||||
MaxActionContext(
|
||||
id=sample_action.id,
|
||||
@@ -142,7 +142,7 @@ async def eval_ui_context_events(call_root_with_ui_context):
|
||||
EvalCase(
|
||||
input={
|
||||
"messages": "Show me trends for this event",
|
||||
"ui_context": MaxContextShape(
|
||||
"ui_context": MaxUIContext(
|
||||
events=[
|
||||
MaxEventContext(
|
||||
id="1",
|
||||
@@ -164,7 +164,7 @@ async def eval_ui_context_events(call_root_with_ui_context):
|
||||
EvalCase(
|
||||
input={
|
||||
"messages": "How many users have triggered these events",
|
||||
"ui_context": MaxContextShape(
|
||||
"ui_context": MaxUIContext(
|
||||
events=[
|
||||
MaxEventContext(
|
||||
id="1",
|
||||
@@ -192,7 +192,7 @@ async def eval_ui_context_events(call_root_with_ui_context):
|
||||
EvalCase(
|
||||
input={
|
||||
"messages": "Create a funnel using these event and action",
|
||||
"ui_context": MaxContextShape(
|
||||
"ui_context": MaxUIContext(
|
||||
events=[
|
||||
MaxEventContext(
|
||||
id="1",
|
||||
|
||||
@@ -12,7 +12,7 @@ from ee.hogai.utils.helpers import find_last_ui_context
|
||||
from ee.models import Conversation, CoreMemory
|
||||
from posthog.models import Team
|
||||
from posthog.models.user import User
|
||||
from posthog.schema import AssistantMessage, AssistantToolCall, MaxContextShape
|
||||
from posthog.schema import AssistantMessage, AssistantToolCall, MaxUIContext
|
||||
from posthog.sync import database_sync_to_async
|
||||
|
||||
from ..utils.types import AssistantMessageUnion, AssistantState, PartialAssistantState
|
||||
@@ -126,7 +126,7 @@ class AssistantNode(ABC):
|
||||
raise ValueError("Contextual tools must be a dictionary of tool names to tool context")
|
||||
return contextual_tools
|
||||
|
||||
def _get_ui_context(self, state: AssistantState) -> MaxContextShape | None:
|
||||
def _get_ui_context(self, state: AssistantState) -> MaxUIContext | None:
|
||||
"""
|
||||
Extracts the UI context from the latest human message.
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,7 @@ from ee.hogai.utils.types import AssistantState
|
||||
from posthog.hogql_queries.query_runner import ExecutionMode
|
||||
from posthog.models import Action
|
||||
from posthog.models.ai.utils import PgEmbeddingRow, bulk_create_pg_embeddings
|
||||
from posthog.schema import MaxActionContext, MaxContextShape, TeamTaxonomyQuery
|
||||
from posthog.schema import MaxActionContext, MaxUIContext, TeamTaxonomyQuery
|
||||
from posthog.test.base import BaseTest, ClickhouseTestMixin
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestInsightRagContextNode(ClickhouseTestMixin, BaseTest):
|
||||
)
|
||||
|
||||
# Mock UI context with actions
|
||||
mock_ui_context = MaxContextShape(
|
||||
mock_ui_context = MaxUIContext(
|
||||
actions=[MaxActionContext(id=context_action.id, name="Context Action", description="From UI Context")]
|
||||
)
|
||||
mock_get_ui_context.return_value = mock_ui_context
|
||||
@@ -117,7 +117,7 @@ class TestInsightRagContextNode(ClickhouseTestMixin, BaseTest):
|
||||
description="Only from context",
|
||||
)
|
||||
|
||||
mock_ui_context = MaxContextShape(
|
||||
mock_ui_context = MaxUIContext(
|
||||
actions=[
|
||||
MaxActionContext(id=context_action.id, name="Context Only Action", description="Only from context")
|
||||
]
|
||||
|
||||
@@ -36,7 +36,7 @@ from posthog.schema import (
|
||||
FunnelsQuery,
|
||||
HogQLQuery,
|
||||
HumanMessage,
|
||||
MaxContextShape,
|
||||
MaxUIContext,
|
||||
MaxInsightContext,
|
||||
RetentionQuery,
|
||||
TrendsQuery,
|
||||
@@ -73,7 +73,7 @@ T = TypeVar("T", RootMessageUnion, BaseMessage)
|
||||
class RootNodeUIContextMixin(AssistantNode):
|
||||
"""Mixin that provides UI context formatting capabilities for root nodes."""
|
||||
|
||||
def _format_ui_context(self, ui_context: Optional[MaxContextShape]) -> str:
|
||||
def _format_ui_context(self, ui_context: Optional[MaxUIContext]) -> str:
|
||||
"""
|
||||
Format UI context into template variables for the prompt.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from posthog.schema import (
|
||||
HumanMessage,
|
||||
LifecycleQuery,
|
||||
MaxActionContext,
|
||||
MaxContextShape,
|
||||
MaxUIContext,
|
||||
MaxDashboardContext,
|
||||
MaxEventContext,
|
||||
MaxInsightContext,
|
||||
@@ -975,7 +975,7 @@ Query results: 42 events
|
||||
)
|
||||
|
||||
# Create mock UI context
|
||||
ui_context = MaxContextShape(dashboards=[dashboard], insights=None)
|
||||
ui_context = MaxUIContext(dashboards=[dashboard], insights=None)
|
||||
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
|
||||
@@ -991,7 +991,7 @@ Query results: 42 events
|
||||
event2 = MaxEventContext(id="2", name="button_click")
|
||||
|
||||
# Create mock UI context
|
||||
ui_context = MaxContextShape(dashboards=None, insights=None, events=[event1, event2], actions=None)
|
||||
ui_context = MaxUIContext(dashboards=None, insights=None, events=[event1, event2], actions=None)
|
||||
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
|
||||
@@ -1004,7 +1004,7 @@ Query results: 42 events
|
||||
event2 = MaxEventContext(id="2", name="button_click", description="User clicked a button")
|
||||
|
||||
# Create mock UI context
|
||||
ui_context = MaxContextShape(dashboards=None, insights=None, events=[event1, event2], actions=None)
|
||||
ui_context = MaxUIContext(dashboards=None, insights=None, events=[event1, event2], actions=None)
|
||||
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
|
||||
@@ -1017,7 +1017,7 @@ Query results: 42 events
|
||||
action2 = MaxActionContext(id=2.0, name="Purchase")
|
||||
|
||||
# Create mock UI context
|
||||
ui_context = MaxContextShape(dashboards=None, insights=None, events=None, actions=[action1, action2])
|
||||
ui_context = MaxUIContext(dashboards=None, insights=None, events=None, actions=[action1, action2])
|
||||
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
|
||||
@@ -1030,7 +1030,7 @@ Query results: 42 events
|
||||
action2 = MaxActionContext(id=2.0, name="Purchase", description="User makes a purchase")
|
||||
|
||||
# Create mock UI context
|
||||
ui_context = MaxContextShape(dashboards=None, insights=None, events=None, actions=[action1, action2])
|
||||
ui_context = MaxUIContext(dashboards=None, insights=None, events=None, actions=[action1, action2])
|
||||
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
|
||||
@@ -1051,7 +1051,7 @@ Query results: 42 events
|
||||
)
|
||||
|
||||
# Create mock UI context
|
||||
ui_context = MaxContextShape(insights=[insight])
|
||||
ui_context = MaxUIContext(insights=[insight])
|
||||
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
|
||||
@@ -1065,7 +1065,7 @@ Query results: 42 events
|
||||
self.assertEqual(result, "")
|
||||
|
||||
# Test with ui_context but no insights
|
||||
ui_context = MaxContextShape(insights=None)
|
||||
ui_context = MaxUIContext(insights=None)
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
self.assertEqual(result, "")
|
||||
|
||||
@@ -1083,7 +1083,7 @@ Query results: 42 events
|
||||
)
|
||||
|
||||
# Create mock UI context
|
||||
ui_context = MaxContextShape(insights=[insight])
|
||||
ui_context = MaxUIContext(insights=[insight])
|
||||
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
|
||||
@@ -1106,7 +1106,7 @@ Query results: 42 events
|
||||
)
|
||||
|
||||
# Create mock UI context
|
||||
ui_context = MaxContextShape(insights=[insight])
|
||||
ui_context = MaxUIContext(insights=[insight])
|
||||
|
||||
result = self.mixin._format_ui_context(ui_context)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ from posthog.schema import (
|
||||
DashboardFilter,
|
||||
FailureMessage,
|
||||
HumanMessage,
|
||||
MaxContextShape,
|
||||
MaxUIContext,
|
||||
MaxDashboardContext,
|
||||
MaxInsightContext,
|
||||
ReasoningMessage,
|
||||
@@ -124,7 +124,7 @@ class TestAssistant(ClickhouseTestMixin, NonAtomicBaseTest):
|
||||
is_new_conversation: bool = False,
|
||||
mode: AssistantMode = AssistantMode.ASSISTANT,
|
||||
contextual_tools: Optional[dict[str, Any]] = None,
|
||||
ui_context: Optional[MaxContextShape] = None,
|
||||
ui_context: Optional[MaxUIContext] = None,
|
||||
) -> tuple[list[tuple[str, Any]], Assistant]:
|
||||
# Create assistant instance with our test graph
|
||||
assistant = Assistant(
|
||||
@@ -980,7 +980,7 @@ class TestAssistant(ClickhouseTestMixin, NonAtomicBaseTest):
|
||||
)
|
||||
|
||||
# Test ui_context with multiple fields
|
||||
ui_context = MaxContextShape(
|
||||
ui_context = MaxUIContext(
|
||||
dashboards=[
|
||||
MaxDashboardContext(
|
||||
id="1",
|
||||
@@ -999,7 +999,7 @@ class TestAssistant(ClickhouseTestMixin, NonAtomicBaseTest):
|
||||
ui_context=ui_context,
|
||||
)
|
||||
|
||||
ui_context_2 = MaxContextShape(insights=[MaxInsightContext(id="3", query=TrendsQuery(series=[]))])
|
||||
ui_context_2 = MaxUIContext(insights=[MaxInsightContext(id="3", query=TrendsQuery(series=[]))])
|
||||
|
||||
# Second run: Create another assistant with the same conversation (simulating retrieval)
|
||||
output2, assistant2 = await self._run_assistant_graph(
|
||||
|
||||
@@ -12,7 +12,7 @@ from posthog.schema import (
|
||||
AssistantMessage,
|
||||
AssistantToolCallMessage,
|
||||
HumanMessage,
|
||||
MaxContextShape,
|
||||
MaxUIContext,
|
||||
VisualizationMessage,
|
||||
)
|
||||
|
||||
@@ -101,7 +101,7 @@ def should_output_assistant_message(candidate_message: AssistantMessageUnion) ->
|
||||
return True
|
||||
|
||||
|
||||
def find_last_ui_context(messages: Sequence[AssistantMessageUnion]) -> MaxContextShape | None:
|
||||
def find_last_ui_context(messages: Sequence[AssistantMessageUnion]) -> MaxUIContext | None:
|
||||
"""Returns the last recorded UI context from all messages."""
|
||||
for message in reversed(messages):
|
||||
if isinstance(message, HumanMessage) and message.ui_context is not None:
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
@@ -154,7 +154,7 @@ import {
|
||||
UserType,
|
||||
} from '~/types'
|
||||
|
||||
import { MaxContextShape } from '../scenes/max/maxTypes'
|
||||
import { MaxUIContext } from '../scenes/max/maxTypes'
|
||||
import { AlertType, AlertTypeWrite } from './components/Alerts/types'
|
||||
import {
|
||||
ErrorTrackingStackFrame,
|
||||
@@ -3539,7 +3539,7 @@ const api = {
|
||||
/** The user message. Null content means we're continuing previous generation. */
|
||||
content: string | null
|
||||
contextual_tools?: Record<string, any>
|
||||
ui_context?: MaxContextShape
|
||||
ui_context?: MaxUIContext
|
||||
conversation?: string | null
|
||||
trace_id: string
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { hide } from '@floating-ui/react'
|
||||
import { IconBadge, IconDashboard, IconEye, IconGraph, IconHide, IconInfo } from '@posthog/icons'
|
||||
import { IconBadge, IconEye, IconHide, IconInfo } from '@posthog/icons'
|
||||
import { LemonButton, LemonDivider, LemonSegmentedButton, LemonSelect, LemonTag } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { ActionPopoverInfo } from 'lib/components/DefinitionPopover/ActionPopoverInfo'
|
||||
@@ -22,7 +22,6 @@ import { Tooltip } from 'lib/lemon-ui/Tooltip'
|
||||
import { cn } from 'lib/utils/css-classes'
|
||||
import { Fragment, useEffect, useMemo } from 'react'
|
||||
import { DataWarehouseTableForInsight } from 'scenes/data-warehouse/types'
|
||||
import { MaxContextOption, MaxDashboardContext, MaxInsightContext } from 'scenes/max/maxTypes'
|
||||
|
||||
import { isCoreFilter } from '~/taxonomy/helpers'
|
||||
import { CORE_FILTER_DEFINITIONS_BY_GROUP } from '~/taxonomy/taxonomy'
|
||||
@@ -374,62 +373,6 @@ function DefinitionView({ group }: { group: TaxonomicFilterGroup }): JSX.Element
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (group.type === TaxonomicFilterGroupType.MaxAIContext) {
|
||||
const _definition = definition as MaxContextOption
|
||||
if (_definition.value !== 'current_page') {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{sharedComponents}
|
||||
{_definition.items?.dashboards && _definition.items.dashboards.length > 0 && (
|
||||
<DefinitionPopover.Section>
|
||||
<DefinitionPopover.Card
|
||||
title="Dashboard"
|
||||
value={
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{_definition.items.dashboards.map((dashboard: MaxDashboardContext) => (
|
||||
<LemonTag
|
||||
key={dashboard.id}
|
||||
size="small"
|
||||
icon={<IconDashboard />}
|
||||
className="text-xs"
|
||||
>
|
||||
{dashboard.name || `Dashboard ${dashboard.id}`}
|
||||
</LemonTag>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</DefinitionPopover.Section>
|
||||
)}
|
||||
{_definition.items?.insights && _definition.items.insights.length > 0 && (
|
||||
<>
|
||||
<LemonDivider className="DefinitionPopover my-4" />
|
||||
<DefinitionPopover.Section>
|
||||
<DefinitionPopover.Card
|
||||
title="Insights"
|
||||
value={
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{_definition.items.insights.map((insight: MaxInsightContext) => (
|
||||
<LemonTag
|
||||
key={insight.id}
|
||||
size="small"
|
||||
icon={<IconGraph />}
|
||||
className="text-xs"
|
||||
>
|
||||
{insight.name || `Insight ${insight.id}`}
|
||||
</LemonTag>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</DefinitionPopover.Section>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (isDataWarehouse) {
|
||||
const _definition = definition as DataWarehouseTableForInsight
|
||||
const columnOptions = Object.values(_definition.fields).map((column) => ({
|
||||
|
||||
@@ -24,7 +24,6 @@ import { isDefinitionStale } from 'lib/utils/definitions'
|
||||
import { useState } from 'react'
|
||||
import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer'
|
||||
import { List, ListRowProps, ListRowRenderer } from 'react-virtualized/dist/es/List'
|
||||
import { MaxContextOption } from 'scenes/max/maxTypes'
|
||||
|
||||
import { EventDefinition, PropertyDefinition } from '~/types'
|
||||
|
||||
@@ -150,7 +149,7 @@ const selectedItemHasPopover = (
|
||||
return (
|
||||
// NB: also update "renderItemContents" above
|
||||
TaxonomicFilterGroupType.EventMetadata,
|
||||
(!!item &&
|
||||
!!item &&
|
||||
!!group?.getValue?.(item) &&
|
||||
!!listGroupType &&
|
||||
([
|
||||
@@ -172,10 +171,7 @@ const selectedItemHasPopover = (
|
||||
TaxonomicFilterGroupType.Metadata,
|
||||
TaxonomicFilterGroupType.SessionProperties,
|
||||
].includes(listGroupType) ||
|
||||
listGroupType.startsWith(TaxonomicFilterGroupType.GroupsPrefix))) ||
|
||||
(!!item &&
|
||||
listGroupType === TaxonomicFilterGroupType.MaxAIContext &&
|
||||
(item as MaxContextOption).value === 'current_page')
|
||||
listGroupType.startsWith(TaxonomicFilterGroupType.GroupsPrefix))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { dataWarehouseJoinsLogic } from 'scenes/data-warehouse/external/dataWarehouseJoinsLogic'
|
||||
import { dataWarehouseSceneLogic } from 'scenes/data-warehouse/settings/dataWarehouseSceneLogic'
|
||||
import { experimentsLogic } from 'scenes/experiments/experimentsLogic'
|
||||
import { MaxContextOption } from 'scenes/max/maxTypes'
|
||||
import { MaxContextTaxonomicFilterOption } from 'scenes/max/maxTypes'
|
||||
import { groupDisplayId } from 'scenes/persons/GroupActorDisplay'
|
||||
import { projectLogic } from 'scenes/projectLogic'
|
||||
import { ReplayTaxonomicFilters } from 'scenes/session-recordings/filters/ReplayTaxonomicFilters'
|
||||
@@ -231,7 +231,7 @@ export const taxonomicFilterLogic = kea<taxonomicFilterLogicType>([
|
||||
propertyFilters: { excludedProperties: any; propertyAllowList: any },
|
||||
eventMetadataPropertyDefinitions: PropertyDefinition[],
|
||||
eventOrdering: string | null,
|
||||
maxContextOptions: MaxContextOption[]
|
||||
maxContextOptions: MaxContextTaxonomicFilterOption[]
|
||||
): TaxonomicFilterGroup[] => {
|
||||
const { excludedProperties, propertyAllowList } = propertyFilters
|
||||
const groups: TaxonomicFilterGroup[] = [
|
||||
@@ -683,9 +683,9 @@ export const taxonomicFilterLogic = kea<taxonomicFilterLogicType>([
|
||||
searchPlaceholder: 'elements from this page',
|
||||
type: TaxonomicFilterGroupType.MaxAIContext,
|
||||
options: maxContextOptions,
|
||||
getName: (option: MaxContextOption) => option.name,
|
||||
getValue: (option: MaxContextOption) => option.value,
|
||||
getIcon: (option: MaxContextOption) => {
|
||||
getName: (option: MaxContextTaxonomicFilterOption) => option.name,
|
||||
getValue: (option: MaxContextTaxonomicFilterOption) => option.value,
|
||||
getIcon: (option: MaxContextTaxonomicFilterOption) => {
|
||||
const Icon = option.icon as React.ComponentType
|
||||
if (Icon) {
|
||||
return <Icon />
|
||||
|
||||
@@ -2,7 +2,7 @@ import Fuse from 'fuse.js'
|
||||
import { LogicWrapper } from 'kea'
|
||||
import { DataWarehouseTableForInsight } from 'scenes/data-warehouse/types'
|
||||
import { LocalFilter } from 'scenes/insights/filters/ActionFilter/entityFilterLogic'
|
||||
import { MaxContextOption } from 'scenes/max/maxTypes'
|
||||
import { MaxContextTaxonomicFilterOption } from 'scenes/max/maxTypes'
|
||||
|
||||
import { AnyDataNode, DatabaseSchemaField, DatabaseSerializedFieldType } from '~/queries/schema/schema-general'
|
||||
import {
|
||||
@@ -47,7 +47,7 @@ export interface TaxonomicFilterProps {
|
||||
hideBehavioralCohorts?: boolean
|
||||
showNumericalPropsOnly?: boolean
|
||||
dataWarehousePopoverFields?: DataWarehousePopoverField[]
|
||||
maxContextOptions?: MaxContextOption[]
|
||||
maxContextOptions?: MaxContextTaxonomicFilterOption[]
|
||||
/**
|
||||
* Controls the layout of taxonomic groups.
|
||||
* When undefined (default), vertical/columnar layout is automatically used when there are more than VERTICAL_LAYOUT_THRESHOLD (4) groups.
|
||||
@@ -186,4 +186,4 @@ export type TaxonomicDefinitionTypes =
|
||||
| ActionType
|
||||
| PersonProperty
|
||||
| DataWarehouseTableForInsight
|
||||
| MaxContextOption
|
||||
| MaxContextTaxonomicFilterOption
|
||||
|
||||
@@ -9,7 +9,7 @@ import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
|
||||
import { LemonDropdown } from 'lib/lemon-ui/LemonDropdown'
|
||||
import { forwardRef, Ref, useEffect, useState } from 'react'
|
||||
import { LocalFilter } from 'scenes/insights/filters/ActionFilter/entityFilterLogic'
|
||||
import { MaxContextOption } from 'scenes/max/maxTypes'
|
||||
import { MaxContextTaxonomicFilterOption } from 'scenes/max/maxTypes'
|
||||
|
||||
import { AnyDataNode, DatabaseSchemaField } from '~/queries/schema/schema-general'
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface TaxonomicPopoverProps<ValueType extends TaxonomicFilterValue =
|
||||
metadataSource?: AnyDataNode
|
||||
showNumericalPropsOnly?: boolean
|
||||
dataWarehousePopoverFields?: DataWarehousePopoverField[]
|
||||
maxContextOptions?: MaxContextOption[]
|
||||
maxContextOptions?: MaxContextTaxonomicFilterOption[]
|
||||
}
|
||||
|
||||
/** Like TaxonomicPopover, but convenient when you know you will only use string values */
|
||||
|
||||
@@ -12817,7 +12817,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ui_context": {
|
||||
"$ref": "#/definitions/MaxContextShape"
|
||||
"$ref": "#/definitions/MaxUIContext"
|
||||
}
|
||||
},
|
||||
"required": ["type", "content"],
|
||||
@@ -14469,62 +14469,27 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "action",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "name"],
|
||||
"type": "object"
|
||||
},
|
||||
"MaxContextShape": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"actions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MaxActionContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dashboards": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MaxDashboardContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"events": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MaxEventContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"filters_override": {
|
||||
"$ref": "#/definitions/DashboardFilter"
|
||||
},
|
||||
"insights": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MaxInsightContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"variables_override": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/HogQLVariable"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": ["type", "id", "name"],
|
||||
"type": "object"
|
||||
},
|
||||
"MaxDashboardContext": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"filters": {
|
||||
"$ref": "#/definitions/DashboardFilter"
|
||||
@@ -14539,26 +14504,34 @@
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"type": {
|
||||
"const": "dashboard",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "insights", "filters"],
|
||||
"required": ["type", "id", "insights", "filters"],
|
||||
"type": "object"
|
||||
},
|
||||
"MaxEventContext": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"type": {
|
||||
"const": "event",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id"],
|
||||
"required": ["type", "id"],
|
||||
"type": "object"
|
||||
},
|
||||
"MaxInnerUniversalFiltersGroup": {
|
||||
@@ -14581,19 +14554,23 @@
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/definitions/InsightShortId"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"query": {
|
||||
"$ref": "#/definitions/QuerySchema"
|
||||
},
|
||||
"type": {
|
||||
"const": "insight",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "query"],
|
||||
"required": ["type", "id", "query"],
|
||||
"type": "object"
|
||||
},
|
||||
"MaxOuterUniversalFiltersGroup": {
|
||||
@@ -14641,6 +14618,45 @@
|
||||
"required": ["duration", "filter_group"],
|
||||
"type": "object"
|
||||
},
|
||||
"MaxUIContext": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"actions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MaxActionContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"dashboards": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MaxDashboardContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"events": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MaxEventContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"filters_override": {
|
||||
"$ref": "#/definitions/DashboardFilter"
|
||||
},
|
||||
"insights": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MaxInsightContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"variables_override": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/HogQLVariable"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MaxUniversalFilterValue": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MaxContextShape } from 'scenes/max/maxTypes'
|
||||
import type { MaxUIContext } from 'scenes/max/maxTypes'
|
||||
|
||||
import {
|
||||
AssistantFunnelsQuery,
|
||||
@@ -23,7 +23,7 @@ export interface BaseAssistantMessage {
|
||||
export interface HumanMessage extends BaseAssistantMessage {
|
||||
type: AssistantMessageType.Human
|
||||
content: string
|
||||
ui_context?: MaxContextShape
|
||||
ui_context?: MaxUIContext
|
||||
}
|
||||
|
||||
export interface AssistantFormOption {
|
||||
|
||||
@@ -16,7 +16,7 @@ import uniqBy from 'lodash.uniqby'
|
||||
import { Layout, Layouts } from 'react-grid-layout'
|
||||
import { calculateLayouts } from 'scenes/dashboard/tileLayouts'
|
||||
import { dataThemeLogic } from 'scenes/dataThemeLogic'
|
||||
import { maxContextLogic } from 'scenes/max/maxContextLogic'
|
||||
import { createMaxContextHelpers, MaxContextInput } from 'scenes/max/maxTypes'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { userLogic } from 'scenes/userLogic'
|
||||
@@ -104,7 +104,7 @@ export const dashboardLogic = kea<dashboardLogicType>([
|
||||
dataThemeLogic,
|
||||
['getTheme'],
|
||||
],
|
||||
logic: [dashboardsModel, insightsModel, eventUsageLogic, variableDataLogic, maxContextLogic],
|
||||
logic: [dashboardsModel, insightsModel, eventUsageLogic, variableDataLogic],
|
||||
})),
|
||||
|
||||
props({} as DashboardLogicProps),
|
||||
@@ -1165,6 +1165,16 @@ export const dashboardLogic = kea<dashboardLogicType>([
|
||||
],
|
||||
// NOTE: noCache is used to prevent the dashboard from using cached results from previous loads when url variables override
|
||||
noCache: [(s) => [s.urlVariables], (urlVariables) => Object.keys(urlVariables).length > 0],
|
||||
maxContext: [
|
||||
(s) => [s.dashboard],
|
||||
(dashboard): MaxContextInput[] => {
|
||||
if (!dashboard) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [createMaxContextHelpers.dashboard(dashboard)]
|
||||
},
|
||||
],
|
||||
})),
|
||||
events(({ actions, cache, props }) => ({
|
||||
afterMount: () => {
|
||||
@@ -1189,8 +1199,6 @@ export const dashboardLogic = kea<dashboardLogicType>([
|
||||
window.clearInterval(cache.autoRefreshInterval)
|
||||
cache.autoRefreshInterval = null
|
||||
}
|
||||
// Clear dashboard context when unmounting
|
||||
maxContextLogic.actions.clearActiveDashboard()
|
||||
},
|
||||
})),
|
||||
sharedListeners(({ values, props }) => ({
|
||||
@@ -1495,9 +1503,6 @@ export const dashboardLogic = kea<dashboardLogicType>([
|
||||
return // We hit a 404
|
||||
}
|
||||
|
||||
// Set dashboard context for Max AI
|
||||
maxContextLogic.actions.setActiveDashboard(values.dashboard)
|
||||
|
||||
// access stored values from dashboardLoadData
|
||||
// as we can't pass them down to this listener
|
||||
const { action, manualDashboardRefresh } = values.dashboardLoadData
|
||||
|
||||
@@ -3,7 +3,6 @@ import { actionToUrl, router } from 'kea-router'
|
||||
import { objectsEqual } from 'lib/utils'
|
||||
import { DATAWAREHOUSE_EDITOR_ITEM_ID } from 'scenes/data-warehouse/utils'
|
||||
import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils'
|
||||
import { maxContextLogic } from 'scenes/max/maxContextLogic'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
import { filterTestAccountsDefaultsLogic } from 'scenes/settings/environment/filterTestAccountDefaultsLogic'
|
||||
|
||||
@@ -60,11 +59,11 @@ export const insightDataLogic = kea<insightDataLogicType>([
|
||||
],
|
||||
actions: [
|
||||
insightLogic,
|
||||
['setInsight', 'setMaxContext'],
|
||||
['setInsight'],
|
||||
dataNodeLogic({ key: insightVizDataNodeKey(props) } as DataNodeLogicProps),
|
||||
['loadData', 'loadDataSuccess', 'loadDataFailure', 'setResponse as setInsightData'],
|
||||
],
|
||||
logic: [insightDataTimingLogic(props), insightUsageLogic(props), maxContextLogic],
|
||||
logic: [insightDataTimingLogic(props), insightUsageLogic(props)],
|
||||
})),
|
||||
|
||||
actions({
|
||||
@@ -216,9 +215,6 @@ export const insightDataLogic = kea<insightDataLogicType>([
|
||||
actions.setInsightData({ ...values.insightData, result: savedResult ? savedResult : null })
|
||||
},
|
||||
setQuery: ({ query }) => {
|
||||
// Update MaxAI context when query changes
|
||||
actions.setMaxContext()
|
||||
|
||||
// if the query is not changed, don't save it
|
||||
if (!query || !values.queryChanged) {
|
||||
return
|
||||
|
||||
@@ -4,7 +4,6 @@ import api from 'lib/api'
|
||||
import { MOCK_DEFAULT_TEAM, MOCK_TEAM_ID } from 'lib/api.mock'
|
||||
import { DashboardPrivilegeLevel, DashboardRestrictionLevel } from 'lib/constants'
|
||||
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
|
||||
import { maxContextLogic } from 'scenes/max/maxContextLogic'
|
||||
import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
@@ -761,99 +760,4 @@ describe('insightLogic', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('maxContext integration', () => {
|
||||
beforeEach(async () => {
|
||||
const insightProps: InsightLogicProps = { dashboardItemId: 'new' }
|
||||
logic = insightLogic(insightProps)
|
||||
logic.mount()
|
||||
|
||||
insightDataLogic(insightProps).mount()
|
||||
})
|
||||
|
||||
it('calls setMaxContext when insight changes via subscription', async () => {
|
||||
const testInsight = {
|
||||
id: 42,
|
||||
short_id: Insight42,
|
||||
name: 'Test Insight',
|
||||
query: { kind: NodeKind.DataTableNode },
|
||||
}
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.setInsight(testInsight, { overrideQuery: true })
|
||||
})
|
||||
.toDispatchActions(['setInsight', 'setMaxContext'])
|
||||
.toFinishAllListeners()
|
||||
})
|
||||
|
||||
it('calls maxContextLogic.addOrUpdateActiveInsight with correct parameters when setMaxContext is triggered', async () => {
|
||||
const testInsight = {
|
||||
id: 42,
|
||||
short_id: Insight42,
|
||||
name: 'Test Insight',
|
||||
query: { kind: NodeKind.DataTableNode },
|
||||
}
|
||||
|
||||
// Set up the insight first
|
||||
logic.actions.setInsight(testInsight, { overrideQuery: true })
|
||||
|
||||
// Mock the maxContextLogic actions
|
||||
const mockAddOrUpdateActiveInsight = jest.fn()
|
||||
jest.spyOn(maxContextLogic, 'findMounted').mockReturnValue({
|
||||
actions: {
|
||||
addOrUpdateActiveInsight: mockAddOrUpdateActiveInsight,
|
||||
},
|
||||
} as any)
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.setMaxContext()
|
||||
})
|
||||
.toDispatchActions(['setMaxContext'])
|
||||
.toFinishAllListeners()
|
||||
|
||||
// Verify that addOrUpdateActiveInsight was called with the correct parameters
|
||||
expect(mockAddOrUpdateActiveInsight).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 42,
|
||||
short_id: Insight42,
|
||||
name: 'Test Insight',
|
||||
query: { kind: NodeKind.DataTableNode },
|
||||
}),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('does not call maxContextLogic.addOrUpdateActiveInsight when insight has no query', async () => {
|
||||
const testInsight = {
|
||||
id: 42,
|
||||
short_id: Insight42,
|
||||
name: 'Test Insight',
|
||||
query: null,
|
||||
}
|
||||
|
||||
// Set up the insight first
|
||||
logic.actions.setInsight(testInsight, { overrideQuery: true })
|
||||
|
||||
// Mock the maxContextLogic actions
|
||||
const mockAddOrUpdateActiveInsight = jest.fn()
|
||||
jest.spyOn(maxContextLogic, 'findMounted').mockReturnValue({
|
||||
actions: {
|
||||
addOrUpdateActiveInsight: mockAddOrUpdateActiveInsight,
|
||||
},
|
||||
} as any)
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.setMaxContext()
|
||||
})
|
||||
.toDispatchActions(['setMaxContext'])
|
||||
.toFinishAllListeners()
|
||||
|
||||
// Verify that addOrUpdateActiveInsight was NOT called because there's no query
|
||||
expect(mockAddOrUpdateActiveInsight).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ import { LemonDialog, LemonInput } from '@posthog/lemon-ui'
|
||||
import { actions, connect, events, kea, key, listeners, LogicWrapper, path, props, reducers, selectors } from 'kea'
|
||||
import { loaders } from 'kea-loaders'
|
||||
import { router } from 'kea-router'
|
||||
import { subscriptions } from 'kea-subscriptions'
|
||||
import { accessLevelSatisfied } from 'lib/components/AccessControlAction'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { LemonField } from 'lib/lemon-ui/LemonField'
|
||||
@@ -14,7 +13,6 @@ import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
|
||||
import { insightSceneLogic } from 'scenes/insights/insightSceneLogic'
|
||||
import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils'
|
||||
import { summarizeInsight } from 'scenes/insights/summarizeInsight'
|
||||
import { maxContextLogic } from 'scenes/max/maxContextLogic'
|
||||
import { savedInsightsLogic } from 'scenes/saved-insights/savedInsightsLogic'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
@@ -125,7 +123,6 @@ export const insightLogic: LogicWrapper<insightLogicType> = kea<insightLogicType
|
||||
}),
|
||||
highlightSeries: (seriesIndex: number | null) => ({ seriesIndex }),
|
||||
setAccessDeniedToInsight: true,
|
||||
setMaxContext: true,
|
||||
}),
|
||||
loaders(({ actions, values, props }) => ({
|
||||
insight: [
|
||||
@@ -501,12 +498,6 @@ export const insightLogic: LogicWrapper<insightLogicType> = kea<insightLogicType
|
||||
router.actions.push(urls.insightEdit(insight.short_id))
|
||||
}
|
||||
},
|
||||
setMaxContext: () => {
|
||||
// Set MaxAI context when insight changes
|
||||
if (values.insight && values.insight.id && values.insight.query) {
|
||||
maxContextLogic.findMounted()?.actions.addOrUpdateActiveInsight(values.insight, values.isInViewMode)
|
||||
}
|
||||
},
|
||||
})),
|
||||
events(({ props, actions }) => ({
|
||||
afterMount: () => {
|
||||
@@ -523,9 +514,4 @@ export const insightLogic: LogicWrapper<insightLogicType> = kea<insightLogicType
|
||||
}
|
||||
},
|
||||
})),
|
||||
subscriptions(({ actions }) => ({
|
||||
insight: () => {
|
||||
actions.setMaxContext()
|
||||
},
|
||||
})),
|
||||
])
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
InsightType,
|
||||
ItemMode,
|
||||
ProjectTreeRef,
|
||||
QueryBasedInsightModel,
|
||||
} from '~/types'
|
||||
|
||||
import { insightDataLogic } from './insightDataLogic'
|
||||
@@ -35,6 +36,8 @@ import { parseDraftQueryFromLocalStorage, parseDraftQueryFromURL } from './utils
|
||||
import api from 'lib/api'
|
||||
import { checkLatestVersionsOnQuery } from '~/queries/utils'
|
||||
|
||||
import { MaxContextInput, createMaxContextHelpers } from 'scenes/max/maxTypes'
|
||||
|
||||
const NEW_INSIGHT = 'new' as const
|
||||
export type InsightId = InsightShortId | typeof NEW_INSIGHT | null
|
||||
|
||||
@@ -233,6 +236,15 @@ export const insightSceneLogic = kea<insightSceneLogicType>([
|
||||
: null
|
||||
},
|
||||
],
|
||||
maxContext: [
|
||||
(s) => [s.insight],
|
||||
(insight: Partial<QueryBasedInsightModel>): MaxContextInput[] => {
|
||||
if (!insight || !insight.short_id || !insight.query) {
|
||||
return []
|
||||
}
|
||||
return [createMaxContextHelpers.insight(insight)]
|
||||
},
|
||||
],
|
||||
})),
|
||||
sharedListeners(({ actions, values }) => ({
|
||||
reloadInsightLogic: () => {
|
||||
|
||||
@@ -14,6 +14,12 @@ function pluralize(count: number, word: string): string {
|
||||
return `${count} ${word}${count > 1 ? 's' : ''}`
|
||||
}
|
||||
|
||||
interface ContextTagItem {
|
||||
type: string
|
||||
name: string
|
||||
icon: React.ReactElement
|
||||
}
|
||||
|
||||
interface ContextSummaryProps {
|
||||
insights?: MaxInsightContext[]
|
||||
dashboards?: MaxDashboardContext[]
|
||||
@@ -75,11 +81,7 @@ export function ContextSummary({
|
||||
}, [contextCounts])
|
||||
|
||||
const allItems = useMemo(() => {
|
||||
const items = []
|
||||
|
||||
if (useCurrentPageContext) {
|
||||
items.push({ type: 'current-page', name: 'Current page', icon: <IconPageChart /> })
|
||||
}
|
||||
const items: ContextTagItem[] = []
|
||||
|
||||
if (dashboards) {
|
||||
dashboards.forEach((dashboard) => {
|
||||
@@ -122,7 +124,7 @@ export function ContextSummary({
|
||||
}
|
||||
|
||||
return items
|
||||
}, [useCurrentPageContext, dashboards, insights, events, actions])
|
||||
}, [dashboards, insights, events, actions])
|
||||
|
||||
if (totalCount === 0) {
|
||||
return null
|
||||
@@ -150,35 +152,13 @@ export function ContextSummary({
|
||||
}
|
||||
|
||||
export function ContextTags({ size = 'default' }: { size?: 'small' | 'default' }): JSX.Element | null {
|
||||
const { contextInsights, contextDashboards, contextEvents, contextActions, useCurrentPageContext } =
|
||||
useValues(maxContextLogic)
|
||||
const {
|
||||
removeContextInsight,
|
||||
removeContextDashboard,
|
||||
removeContextEvent,
|
||||
removeContextAction,
|
||||
disableCurrentPageContext,
|
||||
} = useActions(maxContextLogic)
|
||||
const { contextInsights, contextDashboards, contextEvents, contextActions } = useValues(maxContextLogic)
|
||||
const { removeContextInsight, removeContextDashboard, removeContextEvent, removeContextAction } =
|
||||
useActions(maxContextLogic)
|
||||
|
||||
const allTags = useMemo(() => {
|
||||
const tags: JSX.Element[] = []
|
||||
|
||||
// Current page context
|
||||
if (useCurrentPageContext) {
|
||||
tags.push(
|
||||
<LemonTag
|
||||
key="current-page"
|
||||
icon={<IconPageChart className="flex-shrink-0" />}
|
||||
onClose={disableCurrentPageContext}
|
||||
closable
|
||||
closeOnClick
|
||||
className={clsx('flex items-center', size === 'small' ? 'max-w-20' : 'max-w-48')}
|
||||
>
|
||||
<span className="truncate min-w-0 flex-1">Current page</span>
|
||||
</LemonTag>
|
||||
)
|
||||
}
|
||||
|
||||
// Context items configuration
|
||||
const contextConfigs = [
|
||||
{
|
||||
@@ -237,7 +217,6 @@ export function ContextTags({ size = 'default' }: { size?: 'small' | 'default' }
|
||||
return tags
|
||||
}, [
|
||||
size,
|
||||
useCurrentPageContext,
|
||||
contextDashboards,
|
||||
contextInsights,
|
||||
contextEvents,
|
||||
@@ -246,7 +225,6 @@ export function ContextTags({ size = 'default' }: { size?: 'small' | 'default' }
|
||||
removeContextInsight,
|
||||
removeContextEvent,
|
||||
removeContextAction,
|
||||
disableCurrentPageContext,
|
||||
])
|
||||
|
||||
if (allTags.length === 0) {
|
||||
|
||||
@@ -491,8 +491,7 @@ export const ThreadWithMultipleContextObjects: StoryFn = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const { addOrUpdateContextInsight, enableCurrentPageContext, addOrUpdateActiveInsight } =
|
||||
useActions(maxContextLogic)
|
||||
const { addOrUpdateContextInsight } = useActions(maxContextLogic)
|
||||
|
||||
useEffect(() => {
|
||||
// Add multiple context insights
|
||||
@@ -515,24 +514,7 @@ export const ThreadWithMultipleContextObjects: StoryFn = () => {
|
||||
series: [{ event: 'sign up' }, { event: 'first action' }],
|
||||
} as FunnelsQuery,
|
||||
})
|
||||
|
||||
// Add active insights for current page context
|
||||
addOrUpdateActiveInsight(
|
||||
{
|
||||
short_id: 'active-insight-1' as InsightShortId,
|
||||
name: 'Current Page Metrics',
|
||||
description: 'Metrics for the current page',
|
||||
query: {
|
||||
kind: 'TrendsQuery',
|
||||
series: [{ event: '$pageview' }],
|
||||
} as TrendsQuery,
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
// Enable current page context to show active insights/dashboard
|
||||
enableCurrentPageContext()
|
||||
}, [addOrUpdateActiveInsight, addOrUpdateContextInsight, enableCurrentPageContext])
|
||||
}, [addOrUpdateContextInsight])
|
||||
|
||||
return <Template sidePanel />
|
||||
}
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import { BindLogic, useActions, useValues } from 'kea'
|
||||
import { FEATURE_FLAGS } from 'lib/constants'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { sidePanelLogic } from '~/layout/navigation-3000/sidepanel/sidePanelLogic'
|
||||
|
||||
import { ExpandedFloatingMax } from './components/ExpandedFloatingMax'
|
||||
import { CollapsedFloatingMax } from './components/CollapsedFloatingMax'
|
||||
import { maxGlobalLogic } from './maxGlobalLogic'
|
||||
import { maxLogic } from './maxLogic'
|
||||
import { maxThreadLogic, MaxThreadLogicProps } from './maxThreadLogic'
|
||||
import './MaxFloatingInput.scss'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
import { SidePanelTab } from '~/types'
|
||||
import { useFloatingMaxPosition } from './utils/floatingMaxPositioning'
|
||||
|
||||
export function MaxFloatingInput(): JSX.Element | null {
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
const { sidePanelOpen, selectedTab } = useValues(sidePanelLogic)
|
||||
const { scene, sceneConfig } = useValues(sceneLogic)
|
||||
const { threadLogicKey, conversation } = useValues(maxLogic)
|
||||
|
||||
const { isFloatingMaxExpanded, floatingMaxDragState } = useValues(maxGlobalLogic)
|
||||
const { isFloatingMaxExpanded, floatingMaxDragState, showFloatingMax } = useValues(maxGlobalLogic)
|
||||
const { setFloatingMaxPosition } = useActions(maxGlobalLogic)
|
||||
const { floatingMaxPositionStyle } = useFloatingMaxPosition()
|
||||
|
||||
@@ -46,19 +36,7 @@ export function MaxFloatingInput(): JSX.Element | null {
|
||||
startNewConversation()
|
||||
}
|
||||
|
||||
if (!featureFlags[FEATURE_FLAGS.ARTIFICIAL_HOG] || !featureFlags[FEATURE_FLAGS.FLOATING_ARTIFICIAL_HOG]) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Hide floating Max IF:
|
||||
if (
|
||||
(scene === Scene.Max && !isFloatingMaxExpanded) || // In the full Max scene, and Max is not intentionally in floating mode (i.e. expanded)
|
||||
(sidePanelOpen && selectedTab === SidePanelTab.Max) // The Max side panel is open
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (sceneConfig?.layout === 'plain') {
|
||||
if (!showFloatingMax) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
60
frontend/src/scenes/max/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
### Max Context
|
||||
|
||||
Scene logics can expose a `maxContext` selector to provide relevant context to MaxAI.
|
||||
|
||||
To do so:
|
||||
|
||||
1. Import the necessary types and helpers:
|
||||
|
||||
```typescript
|
||||
import { MaxContextInput, createMaxContextHelpers } from 'scenes/max/maxTypes'
|
||||
```
|
||||
|
||||
2. Add a `maxContext` selector that returns MaxContextInput[]:
|
||||
|
||||
```typescript
|
||||
selectors({
|
||||
maxContext: [
|
||||
(s) => [s.dashboard],
|
||||
(dashboard): MaxContextInput[] => {
|
||||
if (!dashboard) {
|
||||
return []
|
||||
}
|
||||
return [createMaxContextHelpers.dashboard(dashboard)]
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
3. For multiple context items:
|
||||
```typescript
|
||||
maxContext: [
|
||||
(s) => [s.insight, s.events],
|
||||
(insight, events): MaxContextInput[] => {
|
||||
const context = []
|
||||
if (insight) context.push(createMaxContextHelpers.insight(insight))
|
||||
if (events?.length) context.push(...events.map(createMaxContextHelpers.event))
|
||||
return context
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
The maxContextLogic will automatically detect and process these context items.
|
||||
Use the helper functions to ensure type safety and consistency.
|
||||
|
||||
Currently, these context entities are supported:
|
||||
|
||||
- Dashboards
|
||||
- Insights
|
||||
- Events
|
||||
- Actions
|
||||
|
||||
If you want to add new entities, you need to extend `maxContextLogic.ts`, slightly more difficult, but doable, check how other entities are supported and start from there.
|
||||
|
||||
Caveat: we currently support these types of insights: trends, funnels, retention, custom SQL.
|
||||
This means that if you expose a dashboard with custom queries, these will show up in the frontend logic,
|
||||
but won't be actually available to Max in the backend.
|
||||
|
||||
To add support for **reading** custom queries, refer to the README in ee/hogai
|
||||
|
||||
To add support for **generating** insights with custom queries, talk to the Max AI team
|
||||
@@ -1,7 +1,9 @@
|
||||
import { IconDashboard, IconGraph, IconPageChart } from '@posthog/icons'
|
||||
import {} from '@posthog/icons'
|
||||
import { router } from 'kea-router'
|
||||
import { expectLogic, partial } from 'kea-test-utils'
|
||||
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
|
||||
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
|
||||
import { insightLogic } from 'scenes/insights/insightLogic'
|
||||
|
||||
import { useMocks } from '~/mocks/jest'
|
||||
import { initKeaTests } from '~/test/init'
|
||||
@@ -35,6 +37,7 @@ describe('maxContextLogic', () => {
|
||||
name: 'Test Insight',
|
||||
description: 'Test insight description',
|
||||
query: { kind: 'TrendsQuery' },
|
||||
type: 'insight',
|
||||
}
|
||||
|
||||
const mockDashboard: DashboardType<QueryBasedInsightModel> = {
|
||||
@@ -55,12 +58,26 @@ describe('maxContextLogic', () => {
|
||||
description: 'Test event description',
|
||||
} as EventDefinition
|
||||
|
||||
const expectedTransformedEvent = {
|
||||
id: 'event-1',
|
||||
name: 'Test Event',
|
||||
description: 'Test event description',
|
||||
type: 'event',
|
||||
}
|
||||
|
||||
const mockAction: ActionType = {
|
||||
id: 1,
|
||||
name: 'Test Action',
|
||||
description: 'Test action description',
|
||||
} as ActionType
|
||||
|
||||
const expectedTransformedAction = {
|
||||
id: 1,
|
||||
name: 'Test Action',
|
||||
description: 'Test action description',
|
||||
type: 'action',
|
||||
}
|
||||
|
||||
// Create the expected transformed dashboard
|
||||
const expectedTransformedDashboard = {
|
||||
id: 1,
|
||||
@@ -68,6 +85,7 @@ describe('maxContextLogic', () => {
|
||||
description: 'Test dashboard description',
|
||||
insights: [expectedTransformedInsight],
|
||||
filters: mockDashboard.filters,
|
||||
type: 'dashboard',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -82,86 +100,38 @@ describe('maxContextLogic', () => {
|
||||
})
|
||||
|
||||
describe('core functionality', () => {
|
||||
it('manages current page context', async () => {
|
||||
await expectLogic(logic).toMatchValues({
|
||||
useCurrentPageContext: false,
|
||||
})
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.enableCurrentPageContext()
|
||||
}).toMatchValues({
|
||||
useCurrentPageContext: true,
|
||||
})
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.disableCurrentPageContext()
|
||||
}).toMatchValues({
|
||||
useCurrentPageContext: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('manages context data', async () => {
|
||||
await expectLogic(logic).toMatchValues({
|
||||
contextInsights: [],
|
||||
contextDashboards: [],
|
||||
contextEvents: [],
|
||||
contextActions: [],
|
||||
})
|
||||
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight)
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight as any)
|
||||
logic.actions.addOrUpdateContextDashboard(mockDashboard)
|
||||
logic.actions.addOrUpdateContextEvent(mockEvent)
|
||||
logic.actions.addOrUpdateContextAction(mockAction)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
contextInsights: [expectedTransformedInsight],
|
||||
contextDashboards: [expectedTransformedDashboard],
|
||||
})
|
||||
})
|
||||
|
||||
it('manages active insights', async () => {
|
||||
await expectLogic(logic).toMatchValues({
|
||||
activeInsights: [],
|
||||
})
|
||||
|
||||
logic.actions.addOrUpdateActiveInsight(mockInsight, false)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
activeInsights: [expectedTransformedInsight],
|
||||
})
|
||||
|
||||
logic.actions.clearActiveInsights()
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
activeInsights: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('manages active dashboard', async () => {
|
||||
await expectLogic(logic).toMatchValues({
|
||||
activeDashboard: null,
|
||||
})
|
||||
|
||||
logic.actions.setActiveDashboard(mockDashboard)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
activeDashboard: expectedTransformedDashboard,
|
||||
})
|
||||
|
||||
logic.actions.clearActiveDashboard()
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
activeDashboard: null,
|
||||
contextEvents: [expectedTransformedEvent],
|
||||
contextActions: [expectedTransformedAction],
|
||||
})
|
||||
})
|
||||
|
||||
it('resets all context', async () => {
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight)
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight as any)
|
||||
logic.actions.addOrUpdateContextDashboard(mockDashboard)
|
||||
logic.actions.addOrUpdateContextEvent(mockEvent)
|
||||
logic.actions.addOrUpdateContextAction(mockAction)
|
||||
logic.actions.enableCurrentPageContext()
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
contextInsights: [expectedTransformedInsight],
|
||||
contextDashboards: [expectedTransformedDashboard],
|
||||
useCurrentPageContext: true,
|
||||
contextEvents: [expectedTransformedEvent],
|
||||
contextActions: [expectedTransformedAction],
|
||||
})
|
||||
|
||||
logic.actions.resetContext()
|
||||
@@ -171,7 +141,6 @@ describe('maxContextLogic', () => {
|
||||
contextDashboards: [],
|
||||
contextEvents: [],
|
||||
contextActions: [],
|
||||
useCurrentPageContext: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -182,18 +151,16 @@ describe('maxContextLogic', () => {
|
||||
hasData: false,
|
||||
})
|
||||
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight)
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight as any)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
hasData: true,
|
||||
})
|
||||
|
||||
logic.actions.removeContextInsight('test')
|
||||
logic.actions.addOrUpdateActiveInsight(mockInsight, false)
|
||||
logic.actions.enableCurrentPageContext()
|
||||
logic.actions.removeContextInsight('insight-1')
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
hasData: true,
|
||||
hasData: false,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -202,61 +169,8 @@ describe('maxContextLogic', () => {
|
||||
contextOptions: [],
|
||||
})
|
||||
|
||||
logic.actions.addOrUpdateActiveInsight(mockInsight, false)
|
||||
logic.actions.enableCurrentPageContext()
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
contextOptions: [
|
||||
{
|
||||
id: 'current_page',
|
||||
name: 'Current page',
|
||||
value: 'current_page',
|
||||
icon: IconPageChart,
|
||||
items: {
|
||||
insights: [expectedTransformedInsight],
|
||||
dashboards: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'insight-1',
|
||||
name: 'Test Insight',
|
||||
value: 'insight-1',
|
||||
type: 'insight',
|
||||
icon: IconGraph,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
logic.actions.setActiveDashboard(mockDashboard)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
contextOptions: [
|
||||
{
|
||||
id: 'current_page',
|
||||
name: 'Current page',
|
||||
value: 'current_page',
|
||||
icon: IconPageChart,
|
||||
items: {
|
||||
insights: [expectedTransformedInsight],
|
||||
dashboards: [expectedTransformedDashboard],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
name: 'Test Dashboard',
|
||||
value: 1,
|
||||
type: 'dashboard',
|
||||
icon: IconDashboard,
|
||||
},
|
||||
{
|
||||
id: 'insight-1',
|
||||
name: 'Test Insight',
|
||||
value: 'insight-1',
|
||||
type: 'insight',
|
||||
icon: IconGraph,
|
||||
},
|
||||
],
|
||||
})
|
||||
// Since contextOptions now come from sceneContext, we can't test directly
|
||||
// This test would need to be updated to work with scene-based context
|
||||
})
|
||||
|
||||
it('calculates taxonomic group types correctly', async () => {
|
||||
@@ -270,19 +184,8 @@ describe('maxContextLogic', () => {
|
||||
],
|
||||
})
|
||||
|
||||
logic.actions.addOrUpdateActiveInsight(mockInsight, false)
|
||||
logic.actions.enableCurrentPageContext()
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
mainTaxonomicGroupType: TaxonomicFilterGroupType.MaxAIContext,
|
||||
taxonomicGroupTypes: [
|
||||
TaxonomicFilterGroupType.MaxAIContext,
|
||||
TaxonomicFilterGroupType.Events,
|
||||
TaxonomicFilterGroupType.Actions,
|
||||
TaxonomicFilterGroupType.Insights,
|
||||
TaxonomicFilterGroupType.Dashboards,
|
||||
],
|
||||
})
|
||||
// Test would require scene context to have context options
|
||||
// Since contextOptions now come from sceneContext which is computed automatically
|
||||
})
|
||||
|
||||
it('compiles context correctly', async () => {
|
||||
@@ -292,7 +195,7 @@ describe('maxContextLogic', () => {
|
||||
short_id: 'context-insight-1' as any,
|
||||
}
|
||||
|
||||
logic.actions.addOrUpdateContextInsight(contextInsight)
|
||||
logic.actions.addOrUpdateContextInsight(contextInsight as any)
|
||||
logic.actions.addOrUpdateContextDashboard(mockDashboard)
|
||||
logic.actions.addOrUpdateContextEvent(mockEvent)
|
||||
logic.actions.addOrUpdateContextAction(mockAction)
|
||||
@@ -305,6 +208,7 @@ describe('maxContextLogic', () => {
|
||||
name: 'Test Insight',
|
||||
description: 'Test insight description',
|
||||
query: { kind: 'TrendsQuery' },
|
||||
type: 'insight',
|
||||
},
|
||||
],
|
||||
dashboards: [
|
||||
@@ -318,8 +222,11 @@ describe('maxContextLogic', () => {
|
||||
name: 'Test Insight',
|
||||
description: 'Test insight description',
|
||||
query: { kind: 'TrendsQuery' },
|
||||
type: 'insight',
|
||||
},
|
||||
],
|
||||
filters: mockDashboard.filters,
|
||||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
events: [
|
||||
@@ -327,6 +234,7 @@ describe('maxContextLogic', () => {
|
||||
id: 'event-1',
|
||||
name: 'Test Event',
|
||||
description: 'Test event description',
|
||||
type: 'event',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
@@ -334,6 +242,7 @@ describe('maxContextLogic', () => {
|
||||
id: 1,
|
||||
name: 'Test Action',
|
||||
description: 'Test action description',
|
||||
type: 'action',
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -341,7 +250,7 @@ describe('maxContextLogic', () => {
|
||||
})
|
||||
|
||||
it('does not include both insights and dashboard insights when they have same IDs', async () => {
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight)
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight as any)
|
||||
logic.actions.addOrUpdateContextDashboard(mockDashboard)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
@@ -355,10 +264,8 @@ describe('maxContextLogic', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('includes active dashboard when current page context is enabled without insights', async () => {
|
||||
logic.actions.addOrUpdateActiveInsight(mockInsight, false)
|
||||
logic.actions.setActiveDashboard(mockDashboard)
|
||||
logic.actions.enableCurrentPageContext()
|
||||
it('includes dashboards in compiled context', async () => {
|
||||
logic.actions.addOrUpdateContextDashboard(mockDashboard)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
compiledContext: partial({
|
||||
@@ -369,39 +276,23 @@ describe('maxContextLogic', () => {
|
||||
})
|
||||
|
||||
describe('listeners', () => {
|
||||
it('clears active data on location change', async () => {
|
||||
logic.actions.addOrUpdateActiveInsight(mockInsight, false)
|
||||
logic.actions.setActiveDashboard(mockDashboard)
|
||||
it('clears context data on location change', async () => {
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight as any)
|
||||
logic.actions.addOrUpdateContextDashboard(mockDashboard)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
activeInsights: [expectedTransformedInsight],
|
||||
activeDashboard: expectedTransformedDashboard,
|
||||
contextInsights: [expectedTransformedInsight],
|
||||
contextDashboards: [expectedTransformedDashboard],
|
||||
})
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
router.actions.push('/new-path')
|
||||
}).toMatchValues({
|
||||
activeInsights: [],
|
||||
activeDashboard: null,
|
||||
contextInsights: [],
|
||||
contextDashboards: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('handles taxonomic filter change for current page context', async () => {
|
||||
await expectLogic(logic).toMatchValues({
|
||||
useCurrentPageContext: false,
|
||||
})
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.handleTaxonomicFilterChange('current_page', TaxonomicFilterGroupType.MaxAIContext, {
|
||||
id: 'current_page',
|
||||
name: 'Current page',
|
||||
value: 'current_page',
|
||||
icon: IconPageChart,
|
||||
})
|
||||
}).toMatchValues({
|
||||
useCurrentPageContext: true,
|
||||
})
|
||||
})
|
||||
it('handles taxonomic filter change for events', async () => {
|
||||
await expectLogic(logic).toMatchValues({
|
||||
contextEvents: [],
|
||||
@@ -410,7 +301,7 @@ describe('maxContextLogic', () => {
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.handleTaxonomicFilterChange('event-1', TaxonomicFilterGroupType.Events, mockEvent)
|
||||
}).toMatchValues({
|
||||
contextEvents: [mockEvent],
|
||||
contextEvents: [expectedTransformedEvent],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -422,12 +313,12 @@ describe('maxContextLogic', () => {
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.handleTaxonomicFilterChange(1, TaxonomicFilterGroupType.Actions, mockAction)
|
||||
}).toMatchValues({
|
||||
contextActions: [mockAction],
|
||||
contextActions: [expectedTransformedAction],
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves context when only panel parameter changes (side panel opening/closing)', async () => {
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight)
|
||||
logic.actions.addOrUpdateContextInsight(mockInsight as any)
|
||||
logic.actions.addOrUpdateContextDashboard(mockDashboard)
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
@@ -456,4 +347,169 @@ describe('maxContextLogic', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadAndProcessDashboard', () => {
|
||||
const mockDashboardLogicInstance = {
|
||||
mount: jest.fn(),
|
||||
unmount: jest.fn(),
|
||||
actions: {
|
||||
loadDashboard: jest.fn(),
|
||||
},
|
||||
values: {
|
||||
dashboard: mockDashboard,
|
||||
refreshStatus: {},
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(dashboardLogic, 'build').mockReturnValue(mockDashboardLogicInstance as any)
|
||||
mockDashboardLogicInstance.mount.mockClear()
|
||||
mockDashboardLogicInstance.unmount.mockClear()
|
||||
mockDashboardLogicInstance.actions.loadDashboard.mockClear()
|
||||
})
|
||||
|
||||
it('adds preloaded dashboard to context without loading', async () => {
|
||||
const dashboardData = {
|
||||
id: 1,
|
||||
preloaded: mockDashboard,
|
||||
}
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.loadAndProcessDashboard(dashboardData)
|
||||
}).toMatchValues({
|
||||
contextDashboards: [expectedTransformedDashboard],
|
||||
})
|
||||
|
||||
expect(dashboardLogic.build).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('loads dashboard when not preloaded', async () => {
|
||||
const dashboardData = {
|
||||
id: 1,
|
||||
preloaded: null,
|
||||
}
|
||||
|
||||
// Set the mock values that the function will read
|
||||
mockDashboardLogicInstance.values.dashboard = mockDashboard
|
||||
mockDashboardLogicInstance.values.refreshStatus = {}
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.loadAndProcessDashboard(dashboardData)
|
||||
}).toFinishAllListeners()
|
||||
|
||||
expect(dashboardLogic.build).toHaveBeenCalledWith({ id: 1 })
|
||||
expect(mockDashboardLogicInstance.mount).toHaveBeenCalled()
|
||||
expect(mockDashboardLogicInstance.actions.loadDashboard).toHaveBeenCalledWith({ action: 'initial_load' })
|
||||
expect(mockDashboardLogicInstance.unmount).toHaveBeenCalled()
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
contextDashboards: [expectedTransformedDashboard],
|
||||
})
|
||||
})
|
||||
|
||||
it('loads dashboard when preloaded dashboard has no tiles', async () => {
|
||||
const incompleteDashboard = {
|
||||
...mockDashboard,
|
||||
tiles: undefined,
|
||||
}
|
||||
const dashboardData = {
|
||||
id: 1,
|
||||
preloaded: incompleteDashboard as any,
|
||||
}
|
||||
|
||||
// Set the mock values that the function will read
|
||||
mockDashboardLogicInstance.values.dashboard = mockDashboard
|
||||
mockDashboardLogicInstance.values.refreshStatus = {}
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.loadAndProcessDashboard(dashboardData)
|
||||
}).toFinishAllListeners()
|
||||
|
||||
expect(dashboardLogic.build).toHaveBeenCalledWith({ id: 1 })
|
||||
expect(mockDashboardLogicInstance.mount).toHaveBeenCalled()
|
||||
expect(mockDashboardLogicInstance.actions.loadDashboard).toHaveBeenCalledWith({ action: 'initial_load' })
|
||||
expect(mockDashboardLogicInstance.unmount).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadAndProcessInsight', () => {
|
||||
const mockInsightLogicInstance = {
|
||||
mount: jest.fn(),
|
||||
unmount: jest.fn(),
|
||||
actions: {
|
||||
loadInsight: jest.fn(),
|
||||
},
|
||||
values: {
|
||||
insight: mockInsight as QueryBasedInsightModel,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(insightLogic, 'build').mockReturnValue(mockInsightLogicInstance as any)
|
||||
mockInsightLogicInstance.mount.mockClear()
|
||||
mockInsightLogicInstance.unmount.mockClear()
|
||||
mockInsightLogicInstance.actions.loadInsight.mockClear()
|
||||
})
|
||||
|
||||
it('adds preloaded insight to context without loading', async () => {
|
||||
const insightData = {
|
||||
id: 'insight-1' as InsightShortId,
|
||||
preloaded: mockInsight as QueryBasedInsightModel,
|
||||
}
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.loadAndProcessInsight(insightData)
|
||||
}).toMatchValues({
|
||||
contextInsights: [expectedTransformedInsight],
|
||||
})
|
||||
|
||||
expect(insightLogic.build).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('loads insight when not preloaded', async () => {
|
||||
const insightData = {
|
||||
id: 'insight-1' as InsightShortId,
|
||||
preloaded: null,
|
||||
}
|
||||
|
||||
// Set the mock values that the function will read
|
||||
mockInsightLogicInstance.values.insight = mockInsight as QueryBasedInsightModel
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.loadAndProcessInsight(insightData)
|
||||
}).toFinishAllListeners()
|
||||
|
||||
expect(insightLogic.build).toHaveBeenCalledWith({ dashboardItemId: undefined })
|
||||
expect(mockInsightLogicInstance.mount).toHaveBeenCalled()
|
||||
expect(mockInsightLogicInstance.actions.loadInsight).toHaveBeenCalledWith('insight-1')
|
||||
expect(mockInsightLogicInstance.unmount).toHaveBeenCalled()
|
||||
|
||||
await expectLogic(logic).toMatchValues({
|
||||
contextInsights: [expectedTransformedInsight],
|
||||
})
|
||||
})
|
||||
|
||||
it('loads insight when preloaded insight has no query', async () => {
|
||||
const incompleteInsight = {
|
||||
...mockInsight,
|
||||
query: null,
|
||||
}
|
||||
const insightData = {
|
||||
id: 'insight-1' as InsightShortId,
|
||||
preloaded: incompleteInsight as any,
|
||||
}
|
||||
|
||||
// Set the mock values that the function will read
|
||||
mockInsightLogicInstance.values.insight = mockInsight as QueryBasedInsightModel
|
||||
|
||||
await expectLogic(logic, () => {
|
||||
logic.actions.loadAndProcessInsight(insightData)
|
||||
}).toFinishAllListeners()
|
||||
|
||||
expect(insightLogic.build).toHaveBeenCalledWith({ dashboardItemId: undefined })
|
||||
expect(mockInsightLogicInstance.mount).toHaveBeenCalled()
|
||||
expect(mockInsightLogicInstance.actions.loadInsight).toHaveBeenCalledWith('insight-1')
|
||||
expect(mockInsightLogicInstance.unmount).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,105 +1,77 @@
|
||||
import { IconDashboard, IconGraph, IconPageChart } from '@posthog/icons'
|
||||
import { IconDashboard, IconGraph } from '@posthog/icons'
|
||||
import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea'
|
||||
import { router } from 'kea-router'
|
||||
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
|
||||
import { objectsEqual } from 'lib/utils'
|
||||
import { dashboardLogic, RefreshStatus } from 'scenes/dashboard/dashboardLogic'
|
||||
import { insightLogic } from 'scenes/insights/insightLogic'
|
||||
import { insightSceneLogic } from 'scenes/insights/insightSceneLogic'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
|
||||
import { DashboardFilter, HogQLVariable } from '~/queries/schema/schema-general'
|
||||
import { ActionType, DashboardType, EventDefinition, InsightShortId, QueryBasedInsightModel } from '~/types'
|
||||
|
||||
import type { maxContextLogicType } from './maxContextLogicType'
|
||||
import {
|
||||
InsightWithQuery,
|
||||
MaxActionContext,
|
||||
MaxContextOption,
|
||||
MaxContextShape,
|
||||
MaxContextItem,
|
||||
MaxContextTaxonomicFilterOption,
|
||||
MaxUIContext,
|
||||
MaxContextType,
|
||||
MaxDashboardContext,
|
||||
MaxEventContext,
|
||||
MaxInsightContext,
|
||||
MaxContextInput,
|
||||
} from './maxTypes'
|
||||
import { subscriptions } from 'kea-subscriptions'
|
||||
import { dashboardLogic, RefreshStatus } from 'scenes/dashboard/dashboardLogic'
|
||||
import {
|
||||
actionToMaxContextPayload,
|
||||
dashboardToMaxContext,
|
||||
eventToMaxContextPayload,
|
||||
insightToMaxContext,
|
||||
} from './utils'
|
||||
|
||||
// Utility functions for transforming data to max context
|
||||
const insightToMaxContext = (insight: Partial<QueryBasedInsightModel>): MaxInsightContext => {
|
||||
const source = (insight.query as any)?.source
|
||||
return {
|
||||
id: insight.short_id!,
|
||||
name: insight.name,
|
||||
description: insight.description,
|
||||
query: source,
|
||||
}
|
||||
}
|
||||
// Type definitions for better reusability
|
||||
export type TaxonomicItem =
|
||||
| DashboardType
|
||||
| QueryBasedInsightModel
|
||||
| EventDefinition
|
||||
| ActionType
|
||||
| MaxContextTaxonomicFilterOption
|
||||
|
||||
const dashboardToMaxContext = (dashboard: DashboardType<QueryBasedInsightModel>): MaxDashboardContext => {
|
||||
return {
|
||||
id: dashboard.id,
|
||||
name: dashboard.name,
|
||||
description: dashboard.description,
|
||||
insights: dashboard.tiles.filter((tile) => tile.insight).map((tile) => insightToMaxContext(tile.insight!)),
|
||||
filters: dashboard.filters,
|
||||
}
|
||||
}
|
||||
export type DashboardItemInfo = { id: number; preloaded: DashboardType<QueryBasedInsightModel> | null }
|
||||
export type InsightItemInfo = { id: InsightShortId; preloaded: QueryBasedInsightModel | null }
|
||||
|
||||
const eventToMaxContext = (event: EventDefinition): MaxEventContext => {
|
||||
return {
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
}
|
||||
}
|
||||
|
||||
const actionToMaxContext = (action: ActionType): MaxActionContext => {
|
||||
return {
|
||||
id: action.id,
|
||||
name: action.name || `Action ${action.id}`,
|
||||
description: action.description || '',
|
||||
}
|
||||
}
|
||||
type EntityWithIdAndType = { id: string | number; type: string }
|
||||
|
||||
// Generic utility functions for reducers
|
||||
const createAddOrUpdateReducer =
|
||||
<TContext extends { id: string | number }, TInput>(
|
||||
transformer: (input: TInput) => TContext,
|
||||
getId: (input: TInput) => string | number
|
||||
) =>
|
||||
(state: TContext[], input: TInput): TContext[] =>
|
||||
state.filter((item) => item.id !== getId(input)).concat(transformer(input))
|
||||
const sceneContextReducer = <TContext extends EntityWithIdAndType>(
|
||||
type: string,
|
||||
sceneContext: EntityWithIdAndType[]
|
||||
): TContext[] => sceneContext.filter((item): item is TContext => item.type === type)
|
||||
|
||||
const createRemoveReducer =
|
||||
<TContext extends { id: string | number }>() =>
|
||||
(state: TContext[], { id }: { id: string | number }): TContext[] =>
|
||||
state.filter((item) => item.id !== id)
|
||||
const addOrUpdateEntity = <TContext extends EntityWithIdAndType>(state: TContext[], entity: TContext): TContext[] =>
|
||||
state.filter((item) => item.id !== entity.id).concat(entity)
|
||||
|
||||
const createResetReducer =
|
||||
<TContext>() =>
|
||||
(): TContext[] =>
|
||||
[]
|
||||
const removeEntity = <TContext extends EntityWithIdAndType>(state: TContext[], id: string | number): TContext[] =>
|
||||
state.filter((item) => item.id !== id)
|
||||
|
||||
// Generic reducer creator
|
||||
const createEntityReducers = <TContext extends { id: string | number }, TInput>(
|
||||
transformer: (input: TInput) => TContext,
|
||||
getId: (input: TInput) => string | number
|
||||
): {
|
||||
addOrUpdate: (state: TContext[], input: TInput) => TContext[]
|
||||
remove: (state: TContext[], { id }: { id: string | number }) => TContext[]
|
||||
reset: () => TContext[]
|
||||
} => ({
|
||||
addOrUpdate: createAddOrUpdateReducer(transformer, getId),
|
||||
remove: createRemoveReducer<TContext>(),
|
||||
reset: createResetReducer<TContext>(),
|
||||
})
|
||||
export type LoadedEntitiesMap = { dashboard: number[]; insight: string[] }
|
||||
|
||||
export const maxContextLogic = kea<maxContextLogicType>([
|
||||
path(['lib', 'ai', 'maxContextLogic']),
|
||||
connect(() => ({
|
||||
values: [insightSceneLogic, ['filtersOverride', 'variablesOverride']],
|
||||
values: [
|
||||
insightSceneLogic,
|
||||
['filtersOverride', 'variablesOverride'],
|
||||
sceneLogic,
|
||||
['activeScene', 'activeSceneLogic', 'activeLoadedScene'],
|
||||
],
|
||||
actions: [router, ['locationChanged']],
|
||||
})),
|
||||
actions({
|
||||
enableCurrentPageContext: true,
|
||||
disableCurrentPageContext: true,
|
||||
addOrUpdateContextInsight: (data: Partial<QueryBasedInsightModel>) => ({ data }),
|
||||
addOrUpdateContextInsight: (data: InsightWithQuery) => ({ data }),
|
||||
addOrUpdateContextDashboard: (data: DashboardType<QueryBasedInsightModel>) => ({ data }),
|
||||
addOrUpdateContextEvent: (data: EventDefinition) => ({ data }),
|
||||
addOrUpdateContextAction: (data: ActionType) => ({ data }),
|
||||
@@ -107,101 +79,79 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
removeContextDashboard: (id: string | number) => ({ id }),
|
||||
removeContextEvent: (id: string | number) => ({ id }),
|
||||
removeContextAction: (id: string | number) => ({ id }),
|
||||
addOrUpdateActiveInsight: (data: Partial<QueryBasedInsightModel>, autoAdd: boolean) => ({
|
||||
data,
|
||||
autoAdd,
|
||||
}),
|
||||
clearActiveInsights: true,
|
||||
setActiveDashboard: (data: DashboardType<QueryBasedInsightModel>) => ({ data }),
|
||||
clearActiveDashboard: true,
|
||||
loadAndProcessDashboard: (data: DashboardItemInfo) => ({ data }),
|
||||
loadAndProcessInsight: (data: InsightItemInfo) => ({ data }),
|
||||
setSelectedContextOption: (value: string) => ({ value }),
|
||||
handleTaxonomicFilterChange: (
|
||||
value: string | number,
|
||||
groupType: TaxonomicFilterGroupType,
|
||||
item: DashboardType | QueryBasedInsightModel | EventDefinition | ActionType | MaxContextOption
|
||||
item: TaxonomicItem
|
||||
) => ({ value, groupType, item }),
|
||||
resetContext: true,
|
||||
applyContext: (context: MaxContextItem[]) => ({ context }),
|
||||
}),
|
||||
reducers(() => {
|
||||
const insightReducers = createEntityReducers(insightToMaxContext, (insight) => insight.short_id!)
|
||||
const dashboardReducers = createEntityReducers(dashboardToMaxContext, (dashboard) => dashboard.id)
|
||||
const eventReducers = createEntityReducers(eventToMaxContext, (event) => event.id)
|
||||
const actionReducers = createEntityReducers(actionToMaxContext, (action) => action.id)
|
||||
|
||||
return {
|
||||
useCurrentPageContext: [
|
||||
false,
|
||||
{
|
||||
enableCurrentPageContext: () => true,
|
||||
disableCurrentPageContext: () => false,
|
||||
resetContext: () => false,
|
||||
},
|
||||
],
|
||||
contextInsights: [
|
||||
[] as MaxInsightContext[],
|
||||
{
|
||||
addOrUpdateContextInsight: (
|
||||
state: MaxInsightContext[],
|
||||
{ data }: { data: Partial<QueryBasedInsightModel> }
|
||||
) => insightReducers.addOrUpdate(state, data),
|
||||
removeContextInsight: insightReducers.remove,
|
||||
resetContext: insightReducers.reset,
|
||||
addOrUpdateActiveInsight: (
|
||||
state: MaxInsightContext[],
|
||||
{ data, autoAdd }: { data: Partial<QueryBasedInsightModel>; autoAdd: boolean }
|
||||
) => (autoAdd ? insightReducers.addOrUpdate(state, data) : state),
|
||||
},
|
||||
],
|
||||
contextDashboards: [
|
||||
[] as MaxDashboardContext[],
|
||||
{
|
||||
addOrUpdateContextDashboard: (
|
||||
state: MaxDashboardContext[],
|
||||
{ data }: { data: DashboardType<QueryBasedInsightModel> }
|
||||
) => dashboardReducers.addOrUpdate(state, data),
|
||||
removeContextDashboard: dashboardReducers.remove,
|
||||
resetContext: dashboardReducers.reset,
|
||||
},
|
||||
],
|
||||
contextEvents: [
|
||||
[] as MaxEventContext[],
|
||||
{
|
||||
addOrUpdateContextEvent: (state: MaxEventContext[], { data }: { data: EventDefinition }) =>
|
||||
eventReducers.addOrUpdate(state, data),
|
||||
removeContextEvent: eventReducers.remove,
|
||||
resetContext: eventReducers.reset,
|
||||
},
|
||||
],
|
||||
contextActions: [
|
||||
[] as MaxActionContext[],
|
||||
{
|
||||
addOrUpdateContextAction: (state: MaxActionContext[], { data }: { data: ActionType }) =>
|
||||
actionReducers.addOrUpdate(state, data),
|
||||
removeContextAction: actionReducers.remove,
|
||||
resetContext: actionReducers.reset,
|
||||
},
|
||||
],
|
||||
activeInsights: [
|
||||
[] as MaxInsightContext[],
|
||||
{
|
||||
addOrUpdateActiveInsight: (
|
||||
state: MaxInsightContext[],
|
||||
{ data }: { data: Partial<QueryBasedInsightModel> }
|
||||
) => insightReducers.addOrUpdate(state, data),
|
||||
clearActiveInsights: insightReducers.reset,
|
||||
},
|
||||
],
|
||||
activeDashboard: [
|
||||
null as MaxDashboardContext | null,
|
||||
{
|
||||
setActiveDashboard: (_: any, { data }: { data: DashboardType<QueryBasedInsightModel> }) =>
|
||||
dashboardToMaxContext(data),
|
||||
clearActiveDashboard: () => null,
|
||||
},
|
||||
],
|
||||
}
|
||||
reducers({
|
||||
loadedEntities: [
|
||||
{ dashboard: [], insight: [] } as LoadedEntitiesMap,
|
||||
{
|
||||
loadAndProcessInsight: (state: LoadedEntitiesMap, { data }: { data: InsightItemInfo }) => ({
|
||||
...state,
|
||||
insight: [...state.insight, data.id],
|
||||
}),
|
||||
loadAndProcessDashboard: (state: LoadedEntitiesMap, { data }: { data: DashboardItemInfo }) => ({
|
||||
...state,
|
||||
dashboard: [...state.dashboard, data.id],
|
||||
}),
|
||||
},
|
||||
],
|
||||
contextInsights: [
|
||||
[] as MaxInsightContext[],
|
||||
{
|
||||
addOrUpdateContextInsight: (state: MaxInsightContext[], { data }: { data: InsightWithQuery }) =>
|
||||
addOrUpdateEntity(state, insightToMaxContext(data)),
|
||||
removeContextInsight: (state: MaxInsightContext[], { id }: { id: string | number }) =>
|
||||
removeEntity(state, id),
|
||||
applyContext: (_: MaxInsightContext[], { context }: { context: MaxContextItem[] }) =>
|
||||
sceneContextReducer(MaxContextType.INSIGHT, context),
|
||||
},
|
||||
],
|
||||
contextDashboards: [
|
||||
[] as MaxDashboardContext[],
|
||||
{
|
||||
addOrUpdateContextDashboard: (
|
||||
state: MaxDashboardContext[],
|
||||
{ data }: { data: DashboardType<QueryBasedInsightModel> }
|
||||
) => addOrUpdateEntity(state, dashboardToMaxContext(data)),
|
||||
removeContextDashboard: (state: MaxDashboardContext[], { id }: { id: string | number }) =>
|
||||
removeEntity(state, id),
|
||||
applyContext: (_: MaxDashboardContext[], { context }: { context: MaxContextItem[] }) =>
|
||||
sceneContextReducer(MaxContextType.DASHBOARD, context),
|
||||
},
|
||||
],
|
||||
contextEvents: [
|
||||
[] as MaxEventContext[],
|
||||
{
|
||||
addOrUpdateContextEvent: (state: MaxEventContext[], { data }: { data: EventDefinition }) =>
|
||||
addOrUpdateEntity(state, eventToMaxContextPayload(data)),
|
||||
removeContextEvent: (state: MaxEventContext[], { id }: { id: string | number }) =>
|
||||
removeEntity(state, id),
|
||||
applyContext: (_: MaxEventContext[], { context }: { context: MaxContextItem[] }) =>
|
||||
sceneContextReducer(MaxContextType.EVENT, context),
|
||||
},
|
||||
],
|
||||
contextActions: [
|
||||
[] as MaxActionContext[],
|
||||
{
|
||||
addOrUpdateContextAction: (state: MaxActionContext[], { data }: { data: ActionType }) =>
|
||||
addOrUpdateEntity(state, actionToMaxContextPayload(data)),
|
||||
removeContextAction: (state: MaxActionContext[], { id }: { id: string | number }) =>
|
||||
removeEntity(state, id),
|
||||
applyContext: (_: MaxActionContext[], { context }: { context: MaxContextItem[] }) =>
|
||||
sceneContextReducer(MaxContextType.ACTION, context),
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners(({ actions, cache }) => ({
|
||||
listeners(({ actions, cache, values }) => ({
|
||||
locationChanged: () => {
|
||||
// Don't reset context if the only change is the side panel opening/closing
|
||||
const currentLocation = router.values.location
|
||||
@@ -221,8 +171,6 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
|
||||
const shouldResetContext = (): void => {
|
||||
actions.resetContext()
|
||||
actions.clearActiveInsights()
|
||||
actions.clearActiveDashboard()
|
||||
}
|
||||
|
||||
// Always reset context if pathname or search params changed
|
||||
@@ -251,25 +199,73 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
shouldResetContext()
|
||||
}
|
||||
},
|
||||
handleTaxonomicFilterChange: async (
|
||||
{
|
||||
value,
|
||||
groupType,
|
||||
item,
|
||||
}: {
|
||||
value: string | number
|
||||
groupType: TaxonomicFilterGroupType
|
||||
item: DashboardType | QueryBasedInsightModel | EventDefinition | ActionType | MaxContextOption
|
||||
},
|
||||
breakpoint
|
||||
) => {
|
||||
try {
|
||||
// Handle current page context selection
|
||||
if (groupType === TaxonomicFilterGroupType.MaxAIContext && value === 'current_page') {
|
||||
actions.enableCurrentPageContext()
|
||||
return
|
||||
}
|
||||
loadAndProcessDashboard: async ({ data }: { data: DashboardItemInfo }, breakpoint) => {
|
||||
let dashboard = data.preloaded
|
||||
|
||||
if (!dashboard || !dashboard.tiles) {
|
||||
const dashboardLogicInstance = dashboardLogic.build({ id: data.id })
|
||||
dashboardLogicInstance.mount()
|
||||
|
||||
try {
|
||||
dashboardLogicInstance.actions.loadDashboard({ action: 'initial_load' })
|
||||
|
||||
await breakpoint(50)
|
||||
while (!dashboardLogicInstance.values.dashboard) {
|
||||
await breakpoint(50)
|
||||
}
|
||||
|
||||
dashboard = dashboardLogicInstance.values.dashboard
|
||||
|
||||
// Wait for dashboard items to refresh for cached insights
|
||||
while (
|
||||
Object.values(dashboardLogicInstance.values.refreshStatus).some(
|
||||
(status: RefreshStatus) => status.loading
|
||||
)
|
||||
) {
|
||||
await breakpoint(50)
|
||||
}
|
||||
} finally {
|
||||
dashboardLogicInstance.unmount()
|
||||
}
|
||||
}
|
||||
|
||||
if (dashboard) {
|
||||
actions.addOrUpdateContextDashboard(dashboard)
|
||||
}
|
||||
},
|
||||
loadAndProcessInsight: async ({ data }: { data: InsightItemInfo }, breakpoint) => {
|
||||
let insight = data.preloaded
|
||||
|
||||
if (!insight || !insight.query) {
|
||||
const insightLogicInstance = insightLogic.build({ dashboardItemId: undefined })
|
||||
insightLogicInstance.mount()
|
||||
|
||||
try {
|
||||
insightLogicInstance.actions.loadInsight(data.id)
|
||||
|
||||
await breakpoint(50)
|
||||
while (!insightLogicInstance.values.insight.query) {
|
||||
await breakpoint(50)
|
||||
}
|
||||
|
||||
insight = insightLogicInstance.values.insight as QueryBasedInsightModel
|
||||
} finally {
|
||||
insightLogicInstance.unmount()
|
||||
}
|
||||
}
|
||||
|
||||
if (insight) {
|
||||
actions.addOrUpdateContextInsight(insight)
|
||||
}
|
||||
},
|
||||
handleTaxonomicFilterChange: async ({
|
||||
groupType,
|
||||
item,
|
||||
}: {
|
||||
groupType: TaxonomicFilterGroupType
|
||||
item: TaxonomicItem
|
||||
}) => {
|
||||
try {
|
||||
if (groupType === TaxonomicFilterGroupType.Events) {
|
||||
actions.addOrUpdateContextEvent(item as EventDefinition)
|
||||
return
|
||||
@@ -282,19 +278,19 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
const itemInfo = (() => {
|
||||
// Handle MaxAI context with string values like "insight_123" or "dashboard_456"
|
||||
if (groupType === TaxonomicFilterGroupType.MaxAIContext) {
|
||||
const _item = item as MaxContextOption
|
||||
if (_item.type === 'insight') {
|
||||
const _item = item as MaxContextTaxonomicFilterOption
|
||||
if (_item.type === MaxContextType.INSIGHT) {
|
||||
return {
|
||||
type: 'insight',
|
||||
type: MaxContextType.INSIGHT,
|
||||
id: _item.value,
|
||||
preloaded: null,
|
||||
}
|
||||
}
|
||||
if (_item.type === 'dashboard') {
|
||||
if (_item.type === MaxContextType.DASHBOARD) {
|
||||
return isNaN(_item.value as number)
|
||||
? null
|
||||
: {
|
||||
type: 'dashboard',
|
||||
type: MaxContextType.DASHBOARD,
|
||||
id: _item.value,
|
||||
preloaded: null,
|
||||
}
|
||||
@@ -305,7 +301,7 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
if (groupType === TaxonomicFilterGroupType.Dashboards) {
|
||||
const dashboard = item as DashboardType
|
||||
return {
|
||||
type: 'dashboard',
|
||||
type: MaxContextType.DASHBOARD,
|
||||
id: dashboard.id,
|
||||
preloaded: dashboard as DashboardType<QueryBasedInsightModel>,
|
||||
}
|
||||
@@ -314,7 +310,7 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
if (groupType === TaxonomicFilterGroupType.Insights) {
|
||||
const insight = item as QueryBasedInsightModel
|
||||
return {
|
||||
type: 'insight',
|
||||
type: MaxContextType.INSIGHT,
|
||||
id: insight.short_id,
|
||||
preloaded: insight,
|
||||
}
|
||||
@@ -328,118 +324,115 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
}
|
||||
|
||||
// Handle dashboard selection
|
||||
if (itemInfo.type === 'dashboard') {
|
||||
let dashboard = itemInfo.preloaded as DashboardType<QueryBasedInsightModel> | null
|
||||
|
||||
if (!dashboard || !dashboard.tiles) {
|
||||
const dashboardLogicInstance = dashboardLogic.build({ id: itemInfo.id as number })
|
||||
dashboardLogicInstance.mount()
|
||||
|
||||
try {
|
||||
dashboardLogicInstance.actions.loadDashboard({ action: 'initial_load' })
|
||||
|
||||
await breakpoint(50)
|
||||
while (!dashboardLogicInstance.values.dashboard) {
|
||||
await breakpoint(50)
|
||||
}
|
||||
|
||||
dashboard = dashboardLogicInstance.values.dashboard
|
||||
|
||||
// Wait for dashboard items to refresh for cached insights
|
||||
while (
|
||||
Object.values(dashboardLogicInstance.values.refreshStatus).some(
|
||||
(status: RefreshStatus) => status.loading
|
||||
)
|
||||
) {
|
||||
await breakpoint(50)
|
||||
}
|
||||
} finally {
|
||||
dashboardLogicInstance.unmount()
|
||||
}
|
||||
}
|
||||
|
||||
actions.addOrUpdateContextDashboard(dashboard)
|
||||
if (itemInfo.type === MaxContextType.DASHBOARD) {
|
||||
actions.loadAndProcessDashboard({
|
||||
id: itemInfo.id as number,
|
||||
preloaded: itemInfo.preloaded as DashboardType<QueryBasedInsightModel> | null,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle insight selection
|
||||
if (itemInfo.type === 'insight') {
|
||||
let insight = itemInfo.preloaded as QueryBasedInsightModel | null
|
||||
|
||||
if (!insight || !insight.query) {
|
||||
const insightLogicInstance = insightLogic.build({ dashboardItemId: undefined })
|
||||
insightLogicInstance.mount()
|
||||
|
||||
try {
|
||||
insightLogicInstance.actions.loadInsight(itemInfo.id as InsightShortId)
|
||||
|
||||
await breakpoint(50)
|
||||
while (!insightLogicInstance.values.insight.query) {
|
||||
await breakpoint(50)
|
||||
}
|
||||
|
||||
insight = insightLogicInstance.values.insight as QueryBasedInsightModel
|
||||
} finally {
|
||||
insightLogicInstance.unmount()
|
||||
}
|
||||
}
|
||||
|
||||
actions.addOrUpdateContextInsight(insight)
|
||||
if (itemInfo.type === MaxContextType.INSIGHT) {
|
||||
actions.loadAndProcessInsight({
|
||||
id: itemInfo.id as InsightShortId,
|
||||
preloaded: itemInfo.preloaded as QueryBasedInsightModel | null,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling taxonomic filter change:', error)
|
||||
}
|
||||
},
|
||||
resetContext: () => {
|
||||
actions.applyContext(values.sceneContext)
|
||||
},
|
||||
})),
|
||||
selectors({
|
||||
// Automatically collect context from active scene logic
|
||||
// This selector checks if the current scene logic has a 'maxContext' selector
|
||||
// and if so, calls it to get context items for MaxAI
|
||||
rawSceneContext: [
|
||||
() => [
|
||||
// Pass scene selector through to get automatic updates when scene changes
|
||||
(state): MaxContextInput[] => {
|
||||
const activeSceneLogic = sceneLogic.selectors.activeSceneLogic(state, {})
|
||||
|
||||
if (activeSceneLogic && 'maxContext' in activeSceneLogic.selectors) {
|
||||
try {
|
||||
const activeLoadedScene = sceneLogic.selectors.activeLoadedScene(state, {})
|
||||
return activeSceneLogic.selectors.maxContext(
|
||||
state,
|
||||
activeLoadedScene?.paramsToProps?.(activeLoadedScene?.sceneParams) || {}
|
||||
)
|
||||
} catch {
|
||||
// If the maxContext selector fails, return empty array
|
||||
}
|
||||
}
|
||||
return []
|
||||
},
|
||||
],
|
||||
(context: MaxContextItem[]): MaxContextItem[] => context,
|
||||
{ equalityCheck: objectsEqual },
|
||||
],
|
||||
sceneContext: [
|
||||
(s: any) => [s.rawSceneContext],
|
||||
(rawSceneContext: MaxContextInput[]): MaxContextItem[] => {
|
||||
return rawSceneContext
|
||||
.map((item): MaxContextItem | null => {
|
||||
switch (item.type) {
|
||||
case MaxContextType.INSIGHT:
|
||||
return insightToMaxContext(item.data)
|
||||
case MaxContextType.DASHBOARD:
|
||||
return dashboardToMaxContext(item.data)
|
||||
case MaxContextType.EVENT:
|
||||
return eventToMaxContextPayload(item.data)
|
||||
case MaxContextType.ACTION:
|
||||
return actionToMaxContextPayload(item.data)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((item): item is MaxContextItem => item !== null)
|
||||
},
|
||||
],
|
||||
contextOptions: [
|
||||
(s: any) => [s.activeInsights, s.activeDashboard, s.contextInsights, s.contextDashboards],
|
||||
(activeInsights: MaxInsightContext[], activeDashboard: MaxDashboardContext | null): MaxContextOption[] => {
|
||||
const options: MaxContextOption[] = []
|
||||
|
||||
// Add Current page option if there are active items
|
||||
if (activeInsights.length > 0 || activeDashboard) {
|
||||
options.push({
|
||||
id: 'current_page',
|
||||
name: 'Current page',
|
||||
value: 'current_page',
|
||||
icon: IconPageChart,
|
||||
items: {
|
||||
insights: activeInsights,
|
||||
dashboards: activeDashboard ? [activeDashboard] : [],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add individual dashboards from context
|
||||
if (activeDashboard) {
|
||||
options.push({
|
||||
id: activeDashboard.id.toString(),
|
||||
name: activeDashboard.name || `Dashboard ${activeDashboard.id}`,
|
||||
value: activeDashboard.id,
|
||||
type: 'dashboard',
|
||||
icon: IconDashboard,
|
||||
})
|
||||
}
|
||||
|
||||
// Add individual insights from context
|
||||
if (activeInsights.length > 0) {
|
||||
activeInsights.forEach((insight) => {
|
||||
(s: any) => [s.sceneContext],
|
||||
(sceneContext: MaxContextItem[]): MaxContextTaxonomicFilterOption[] => {
|
||||
const options: MaxContextTaxonomicFilterOption[] = []
|
||||
sceneContext.forEach((item) => {
|
||||
if (item.type == MaxContextType.INSIGHT) {
|
||||
options.push({
|
||||
id: insight.id.toString(),
|
||||
name: insight.name || `Insight ${insight.id}`,
|
||||
value: insight.id,
|
||||
type: 'insight',
|
||||
id: item.id.toString(),
|
||||
name: item.name || `Insight ${item.id}`,
|
||||
value: item.id,
|
||||
type: MaxContextType.INSIGHT,
|
||||
icon: IconGraph,
|
||||
})
|
||||
})
|
||||
}
|
||||
} else if (item.type == MaxContextType.DASHBOARD) {
|
||||
options.push({
|
||||
id: item.id.toString(),
|
||||
name: item.name || `Dashboard ${item.id}`,
|
||||
value: item.id,
|
||||
type: MaxContextType.DASHBOARD,
|
||||
icon: IconDashboard,
|
||||
})
|
||||
item.insights.forEach((insight) => {
|
||||
options.push({
|
||||
id: insight.id.toString(),
|
||||
name: insight.name || `Insight ${insight.id}`,
|
||||
value: insight.id,
|
||||
type: MaxContextType.INSIGHT,
|
||||
icon: IconGraph,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return options
|
||||
},
|
||||
],
|
||||
mainTaxonomicGroupType: [
|
||||
(s: any) => [s.contextOptions],
|
||||
(contextOptions: MaxContextOption[]): TaxonomicFilterGroupType => {
|
||||
(contextOptions: MaxContextTaxonomicFilterOption[]): TaxonomicFilterGroupType => {
|
||||
return contextOptions.length > 0
|
||||
? TaxonomicFilterGroupType.MaxAIContext
|
||||
: TaxonomicFilterGroupType.Events
|
||||
@@ -447,7 +440,7 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
],
|
||||
taxonomicGroupTypes: [
|
||||
(s: any) => [s.contextOptions],
|
||||
(contextOptions: MaxContextOption[]): TaxonomicFilterGroupType[] => {
|
||||
(contextOptions: MaxContextTaxonomicFilterOption[]): TaxonomicFilterGroupType[] => {
|
||||
const groupTypes: TaxonomicFilterGroupType[] = []
|
||||
if (contextOptions.length > 0) {
|
||||
groupTypes.push(TaxonomicFilterGroupType.MaxAIContext)
|
||||
@@ -468,9 +461,6 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
s.contextDashboards,
|
||||
s.contextEvents,
|
||||
s.contextActions,
|
||||
s.useCurrentPageContext,
|
||||
s.activeInsights,
|
||||
s.activeDashboard,
|
||||
s.filtersOverride,
|
||||
s.variablesOverride,
|
||||
],
|
||||
@@ -480,28 +470,19 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
contextDashboards: MaxDashboardContext[],
|
||||
contextEvents: MaxEventContext[],
|
||||
contextActions: MaxActionContext[],
|
||||
useCurrentPageContext: boolean,
|
||||
activeInsights: MaxInsightContext[],
|
||||
activeDashboard: MaxDashboardContext | null,
|
||||
filtersOverride: DashboardFilter,
|
||||
variablesOverride: Record<string, HogQLVariable> | null
|
||||
): MaxContextShape | null => {
|
||||
const context: MaxContextShape = {}
|
||||
): MaxUIContext | null => {
|
||||
const context: MaxUIContext = {}
|
||||
|
||||
// Add context dashboards
|
||||
if (Object.keys(contextDashboards).length > 0) {
|
||||
context.dashboards = Object.values(contextDashboards)
|
||||
}
|
||||
|
||||
// Add active dashboard if useCurrentPageContext is true
|
||||
if (useCurrentPageContext && activeDashboard) {
|
||||
context.dashboards = Object.values(context.dashboards || {}).concat(activeDashboard)
|
||||
// Add context dashboards (combine manual context + scene context)
|
||||
if (contextDashboards.length > 0) {
|
||||
context.dashboards = contextDashboards
|
||||
}
|
||||
|
||||
// Add insights, filtering out those already in dashboards
|
||||
const allInsights = useCurrentPageContext
|
||||
? [...(activeInsights || []), ...(contextInsights || [])]
|
||||
: contextInsights
|
||||
// Combine manual context, scene context, and active insights
|
||||
const allInsights = contextInsights
|
||||
|
||||
if (allInsights.length > 0) {
|
||||
// Get all insight IDs from dashboards to filter out duplicates
|
||||
@@ -545,10 +526,13 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
context.insights = Array.from(uniqueInsights.values())
|
||||
}
|
||||
|
||||
if (Object.keys(contextEvents).length > 0) {
|
||||
// Add events
|
||||
if (contextEvents.length > 0) {
|
||||
context.events = contextEvents
|
||||
}
|
||||
if (Object.keys(contextActions).length > 0) {
|
||||
|
||||
// Add actions
|
||||
if (contextActions.length > 0) {
|
||||
context.actions = contextActions
|
||||
}
|
||||
|
||||
@@ -556,35 +540,42 @@ export const maxContextLogic = kea<maxContextLogicType>([
|
||||
},
|
||||
],
|
||||
hasData: [
|
||||
(s: any) => [
|
||||
s.contextInsights,
|
||||
s.contextDashboards,
|
||||
s.contextEvents,
|
||||
s.contextActions,
|
||||
s.useCurrentPageContext,
|
||||
s.activeInsights,
|
||||
s.activeDashboard,
|
||||
],
|
||||
(s: any) => [s.contextInsights, s.contextDashboards, s.contextEvents, s.contextActions],
|
||||
(
|
||||
contextInsights: MaxInsightContext[],
|
||||
contextDashboards: MaxDashboardContext[],
|
||||
contextEvents: MaxEventContext[],
|
||||
contextActions: MaxActionContext[],
|
||||
useCurrentPageContext: boolean,
|
||||
activeInsights: MaxInsightContext[],
|
||||
activeDashboard: MaxDashboardContext | null
|
||||
contextActions: MaxActionContext[]
|
||||
): boolean => {
|
||||
return (
|
||||
contextInsights.length > 0 ||
|
||||
contextDashboards.length > 0 ||
|
||||
contextEvents.length > 0 ||
|
||||
contextActions.length > 0 ||
|
||||
(useCurrentPageContext && activeInsights && activeInsights.length > 0) ||
|
||||
(useCurrentPageContext && activeDashboard !== null)
|
||||
)
|
||||
return [contextInsights, contextDashboards, contextEvents, contextActions].some((arr) => arr.length > 0)
|
||||
},
|
||||
],
|
||||
}),
|
||||
subscriptions(({ values, actions }) => ({
|
||||
rawSceneContext: (rawContext: MaxContextInput[]) => {
|
||||
rawContext.forEach((item: MaxContextInput) => {
|
||||
if (
|
||||
item.type === MaxContextType.INSIGHT &&
|
||||
item.data.short_id &&
|
||||
!values.loadedEntities.insight.includes(item.data.short_id)
|
||||
) {
|
||||
actions.loadAndProcessInsight({
|
||||
id: item.data.short_id,
|
||||
preloaded: item.data as QueryBasedInsightModel,
|
||||
})
|
||||
} else if (
|
||||
item.type === MaxContextType.DASHBOARD &&
|
||||
item.data.id &&
|
||||
!values.loadedEntities.dashboard.includes(item.data.id)
|
||||
) {
|
||||
actions.loadAndProcessDashboard({ id: item.data.id, preloaded: item.data })
|
||||
}
|
||||
})
|
||||
},
|
||||
sceneContext: (context: MaxContextItem[]) => {
|
||||
actions.applyContext(context)
|
||||
},
|
||||
})),
|
||||
afterMount(({ cache }) => {
|
||||
cache.previousLocation = {
|
||||
location: router.values.location,
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea'
|
||||
|
||||
import { OrganizationMembershipLevel } from 'lib/constants'
|
||||
import { FEATURE_FLAGS, OrganizationMembershipLevel } from 'lib/constants'
|
||||
import { organizationLogic } from 'scenes/organizationLogic'
|
||||
|
||||
import type { maxGlobalLogicType } from './maxGlobalLogicType'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { urls } from 'scenes/urls'
|
||||
import { router } from 'kea-router'
|
||||
import { AssistantContextualTool, AssistantNavigateUrls } from '~/queries/schema/schema-assistant-messages'
|
||||
import { sceneLogic } from 'scenes/sceneLogic'
|
||||
import { routes } from 'scenes/scenes'
|
||||
import { IconCompass } from '@posthog/icons'
|
||||
import { Scene } from 'scenes/sceneTypes'
|
||||
import { SidePanelTab } from '~/types'
|
||||
import { sidePanelLogic } from '~/layout/navigation-3000/sidepanel/sidePanelLogic'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
|
||||
export interface ToolDefinition {
|
||||
/** A unique identifier for the tool */
|
||||
@@ -45,7 +49,14 @@ export interface ToolDefinition {
|
||||
export const maxGlobalLogic = kea<maxGlobalLogicType>([
|
||||
path(['scenes', 'max', 'maxGlobalLogic']),
|
||||
connect(() => ({
|
||||
values: [organizationLogic, ['currentOrganization']],
|
||||
values: [
|
||||
organizationLogic,
|
||||
['currentOrganization'],
|
||||
sceneLogic,
|
||||
['scene', 'sceneConfig'],
|
||||
featureFlagLogic,
|
||||
['featureFlags'],
|
||||
],
|
||||
actions: [router, ['locationChanged']],
|
||||
})),
|
||||
actions({
|
||||
@@ -150,6 +161,24 @@ export const maxGlobalLogic = kea<maxGlobalLogicType>([
|
||||
},
|
||||
})),
|
||||
selectors({
|
||||
showFloatingMax: [
|
||||
(s) => [
|
||||
s.scene,
|
||||
s.sceneConfig,
|
||||
s.isFloatingMaxExpanded,
|
||||
sidePanelLogic.selectors.sidePanelOpen,
|
||||
sidePanelLogic.selectors.selectedTab,
|
||||
s.featureFlags,
|
||||
],
|
||||
(scene, sceneConfig, isFloatingMaxExpanded, sidePanelOpen, selectedTab, featureFlags) =>
|
||||
sceneConfig &&
|
||||
!sceneConfig.onlyUnauthenticated &&
|
||||
sceneConfig.layout !== 'plain' &&
|
||||
!(scene === Scene.Max && !isFloatingMaxExpanded) && // In the full Max scene, and Max is not intentionally in floating mode (i.e. expanded)
|
||||
!(sidePanelOpen && selectedTab === SidePanelTab.Max) && // The Max side panel is open
|
||||
featureFlags[FEATURE_FLAGS.ARTIFICIAL_HOG] &&
|
||||
featureFlags[FEATURE_FLAGS.FLOATING_ARTIFICIAL_HOG],
|
||||
],
|
||||
dataProcessingAccepted: [
|
||||
(s) => [s.currentOrganization],
|
||||
(currentOrganization): boolean => !!currentOrganization?.is_ai_data_processing_approved,
|
||||
|
||||
@@ -21,15 +21,6 @@ describe('maxLogic', () => {
|
||||
logic?.unmount()
|
||||
})
|
||||
|
||||
it("doesn't mount sidePanelStateLogic if it's not already mounted", async () => {
|
||||
// Mount maxLogic after setting up the sidePanelStateLogic state
|
||||
logic = maxLogic()
|
||||
logic.mount()
|
||||
|
||||
// Check that sidePanelStateLogic was not mounted
|
||||
expect(sidePanelStateLogic.isMounted()).toBe(false)
|
||||
})
|
||||
|
||||
it('sets the question when URL has hash param #panel=max:Foo', async () => {
|
||||
// Set up router with #panel=max:Foo
|
||||
router.actions.push('', {}, { panel: 'max:Foo' })
|
||||
|
||||
@@ -1,37 +1,49 @@
|
||||
import { DashboardFilter, HogQLVariable, QuerySchema } from '~/queries/schema/schema-general'
|
||||
import { integer } from '~/queries/schema/type-utils'
|
||||
import { InsightShortId } from '~/types'
|
||||
import { ActionType, DashboardType, EventDefinition, InsightShortId, QueryBasedInsightModel } from '~/types'
|
||||
|
||||
export enum MaxContextType {
|
||||
DASHBOARD = 'dashboard',
|
||||
INSIGHT = 'insight',
|
||||
EVENT = 'event',
|
||||
ACTION = 'action',
|
||||
}
|
||||
|
||||
export type InsightWithQuery = Pick<Partial<QueryBasedInsightModel>, 'query'> & Partial<QueryBasedInsightModel>
|
||||
|
||||
export interface MaxInsightContext {
|
||||
type: MaxContextType.INSIGHT
|
||||
id: InsightShortId
|
||||
name?: string
|
||||
description?: string
|
||||
|
||||
name?: string | null
|
||||
description?: string | null
|
||||
query: QuerySchema // The actual query node, e.g., TrendsQuery, HogQLQuery
|
||||
}
|
||||
|
||||
export interface MaxDashboardContext {
|
||||
type: MaxContextType.DASHBOARD
|
||||
id: number
|
||||
name?: string
|
||||
description?: string
|
||||
name?: string | null
|
||||
description?: string | null
|
||||
insights: MaxInsightContext[]
|
||||
filters: DashboardFilter
|
||||
}
|
||||
|
||||
export interface MaxEventContext {
|
||||
type: MaxContextType.EVENT
|
||||
id: string
|
||||
name?: string
|
||||
description?: string
|
||||
name?: string | null
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
export interface MaxActionContext {
|
||||
type: MaxContextType.ACTION
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
// The main shape for the UI context sent to the backend
|
||||
export interface MaxContextShape {
|
||||
export interface MaxUIContext {
|
||||
dashboards?: MaxDashboardContext[]
|
||||
insights?: MaxInsightContext[]
|
||||
events?: MaxEventContext[]
|
||||
@@ -41,14 +53,61 @@ export interface MaxContextShape {
|
||||
}
|
||||
|
||||
// Taxonomic filter options
|
||||
export interface MaxContextOption {
|
||||
export interface MaxContextTaxonomicFilterOption {
|
||||
id: string
|
||||
value: string | integer
|
||||
name: string
|
||||
icon: React.ReactNode
|
||||
type?: 'dashboard' | 'insight'
|
||||
items?: {
|
||||
insights?: MaxInsightContext[]
|
||||
dashboards?: MaxDashboardContext[]
|
||||
}
|
||||
type?: MaxContextType
|
||||
}
|
||||
|
||||
// Union type for all possible context payloads that can be exposed by scene logics
|
||||
export type MaxContextItem = MaxInsightContext | MaxDashboardContext | MaxEventContext | MaxActionContext
|
||||
|
||||
type MaxInsightContextInput = {
|
||||
type: MaxContextType.INSIGHT
|
||||
data: InsightWithQuery
|
||||
}
|
||||
type MaxDashboardContextInput = {
|
||||
type: MaxContextType.DASHBOARD
|
||||
data: DashboardType<QueryBasedInsightModel>
|
||||
}
|
||||
type MaxEventContextInput = {
|
||||
type: MaxContextType.EVENT
|
||||
data: EventDefinition
|
||||
}
|
||||
type MaxActionContextInput = {
|
||||
type: MaxContextType.ACTION
|
||||
data: ActionType
|
||||
}
|
||||
export type MaxContextInput =
|
||||
| MaxInsightContextInput
|
||||
| MaxDashboardContextInput
|
||||
| MaxEventContextInput
|
||||
| MaxActionContextInput
|
||||
|
||||
/**
|
||||
* Helper functions to create maxContext items safely
|
||||
* These ensure proper typing and consistent patterns across scene logics
|
||||
*/
|
||||
export const createMaxContextHelpers = {
|
||||
dashboard: (dashboard: DashboardType<QueryBasedInsightModel>): MaxDashboardContextInput => ({
|
||||
type: MaxContextType.DASHBOARD,
|
||||
data: dashboard,
|
||||
}),
|
||||
|
||||
insight: (insight: InsightWithQuery): MaxInsightContextInput => ({
|
||||
type: MaxContextType.INSIGHT,
|
||||
data: insight,
|
||||
}),
|
||||
|
||||
event: (event: EventDefinition): MaxEventContextInput => ({
|
||||
type: MaxContextType.EVENT,
|
||||
data: event,
|
||||
}),
|
||||
|
||||
action: (action: ActionType): MaxActionContextInput => ({
|
||||
type: MaxContextType.ACTION,
|
||||
data: action,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
} from '~/queries/schema/schema-assistant-queries'
|
||||
import { FunnelsQuery, HogQLQuery, RetentionQuery, TrendsQuery } from '~/queries/schema/schema-general'
|
||||
import { isFunnelsQuery, isHogQLQuery, isRetentionQuery, isTrendsQuery } from '~/queries/utils'
|
||||
import { SidePanelTab } from '~/types'
|
||||
import { ActionType, DashboardType, EventDefinition, QueryBasedInsightModel, SidePanelTab } from '~/types'
|
||||
import { MaxActionContext, MaxContextType, MaxDashboardContext, MaxEventContext, MaxInsightContext } from './maxTypes'
|
||||
|
||||
export function isReasoningMessage(message: RootAssistantMessage | undefined | null): message is ReasoningMessage {
|
||||
return message?.type === AssistantMessageType.Reasoning
|
||||
@@ -166,3 +167,44 @@ export function generateBurstPoints(spikeCount: number, spikiness: number): stri
|
||||
|
||||
return points.trim()
|
||||
}
|
||||
|
||||
// Utility functions for transforming data to max context
|
||||
export const insightToMaxContext = (insight: Partial<QueryBasedInsightModel>): MaxInsightContext => {
|
||||
const source = (insight.query as any)?.source
|
||||
return {
|
||||
type: MaxContextType.INSIGHT,
|
||||
id: insight.short_id!,
|
||||
name: insight.name || insight.derived_name,
|
||||
description: insight.description,
|
||||
query: source,
|
||||
}
|
||||
}
|
||||
|
||||
export const dashboardToMaxContext = (dashboard: DashboardType<QueryBasedInsightModel>): MaxDashboardContext => {
|
||||
return {
|
||||
type: MaxContextType.DASHBOARD,
|
||||
id: dashboard.id,
|
||||
name: dashboard.name,
|
||||
description: dashboard.description,
|
||||
insights: dashboard.tiles.filter((tile) => tile.insight).map((tile) => insightToMaxContext(tile.insight!)),
|
||||
filters: dashboard.filters,
|
||||
}
|
||||
}
|
||||
|
||||
export const eventToMaxContextPayload = (event: EventDefinition): MaxEventContext => {
|
||||
return {
|
||||
type: MaxContextType.EVENT,
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
}
|
||||
}
|
||||
|
||||
export const actionToMaxContextPayload = (action: ActionType): MaxActionContext => {
|
||||
return {
|
||||
type: MaxContextType.ACTION,
|
||||
id: action.id,
|
||||
name: action.name || `Action ${action.id}`,
|
||||
description: action.description || '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { connect, kea, path, selectors } from 'kea'
|
||||
import { loaders } from 'kea-loaders'
|
||||
import api from 'lib/api'
|
||||
import { DashboardLogicProps } from 'scenes/dashboard/dashboardLogic'
|
||||
import { dashboardLogic, DashboardLogicProps } from 'scenes/dashboard/dashboardLogic'
|
||||
import { MaxContextInput, createMaxContextHelpers } from 'scenes/max/maxTypes'
|
||||
import { projectLogic } from 'scenes/projectLogic'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
|
||||
import { getQueryBasedInsightModel } from '~/queries/nodes/InsightViz/utils'
|
||||
import { DashboardPlacement, InsightModel, QueryBasedInsightModel } from '~/types'
|
||||
import { DashboardPlacement, DashboardType, InsightModel, QueryBasedInsightModel } from '~/types'
|
||||
|
||||
import type { projectHomepageLogicType } from './projectHomepageLogicType'
|
||||
|
||||
@@ -28,6 +29,28 @@ export const projectHomepageLogic = kea<projectHomepageLogicType>([
|
||||
}
|
||||
: null,
|
||||
],
|
||||
maxContext: [
|
||||
(s) => [
|
||||
(state) => {
|
||||
// Get the dashboard from the mounted dashboardLogic
|
||||
const dashboardLogicProps = s.dashboardLogicProps(state)
|
||||
if (!dashboardLogicProps) {
|
||||
return null
|
||||
}
|
||||
const logic = dashboardLogic.findMounted(dashboardLogicProps)
|
||||
if (!logic) {
|
||||
return null
|
||||
}
|
||||
return logic.selectors.dashboard(state)
|
||||
},
|
||||
],
|
||||
(dashboard: DashboardType<QueryBasedInsightModel> | null): MaxContextInput[] => {
|
||||
if (!dashboard) {
|
||||
return []
|
||||
}
|
||||
return [createMaxContextHelpers.dashboard(dashboard)]
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
loaders(({ values }) => ({
|
||||
|
||||
@@ -1628,6 +1628,7 @@ class MaxActionContext(BaseModel):
|
||||
description: Optional[str] = None
|
||||
id: float
|
||||
name: str
|
||||
type: Literal["action"] = "action"
|
||||
|
||||
|
||||
class MaxEventContext(BaseModel):
|
||||
@@ -1637,6 +1638,7 @@ class MaxEventContext(BaseModel):
|
||||
description: Optional[str] = None
|
||||
id: str
|
||||
name: Optional[str] = None
|
||||
type: Literal["event"] = "event"
|
||||
|
||||
|
||||
class MinimalHedgehogConfig(BaseModel):
|
||||
@@ -12309,19 +12311,7 @@ class HumanMessage(BaseModel):
|
||||
content: str
|
||||
id: Optional[str] = None
|
||||
type: Literal["human"] = "human"
|
||||
ui_context: Optional[MaxContextShape] = None
|
||||
|
||||
|
||||
class MaxContextShape(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
extra="forbid",
|
||||
)
|
||||
actions: Optional[list[MaxActionContext]] = None
|
||||
dashboards: Optional[list[MaxDashboardContext]] = None
|
||||
events: Optional[list[MaxEventContext]] = None
|
||||
filters_override: Optional[DashboardFilter] = None
|
||||
insights: Optional[list[MaxInsightContext]] = None
|
||||
variables_override: Optional[dict[str, HogQLVariable]] = None
|
||||
ui_context: Optional[MaxUIContext] = None
|
||||
|
||||
|
||||
class MaxDashboardContext(BaseModel):
|
||||
@@ -12333,6 +12323,7 @@ class MaxDashboardContext(BaseModel):
|
||||
id: float
|
||||
insights: list[MaxInsightContext]
|
||||
name: Optional[str] = None
|
||||
type: Literal["dashboard"] = "dashboard"
|
||||
|
||||
|
||||
class MaxInsightContext(BaseModel):
|
||||
@@ -12401,6 +12392,19 @@ class MaxInsightContext(BaseModel):
|
||||
TracesQuery,
|
||||
VectorSearchQuery,
|
||||
] = Field(..., discriminator="kind")
|
||||
type: Literal["insight"] = "insight"
|
||||
|
||||
|
||||
class MaxUIContext(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
extra="forbid",
|
||||
)
|
||||
actions: Optional[list[MaxActionContext]] = None
|
||||
dashboards: Optional[list[MaxDashboardContext]] = None
|
||||
events: Optional[list[MaxEventContext]] = None
|
||||
filters_override: Optional[DashboardFilter] = None
|
||||
insights: Optional[list[MaxInsightContext]] = None
|
||||
variables_override: Optional[dict[str, HogQLVariable]] = None
|
||||
|
||||
|
||||
class QueryRequest(BaseModel):
|
||||
@@ -12843,7 +12847,6 @@ class SourceFieldSwitchGroupConfig(BaseModel):
|
||||
|
||||
PropertyGroupFilterValue.model_rebuild()
|
||||
HumanMessage.model_rebuild()
|
||||
MaxContextShape.model_rebuild()
|
||||
MaxDashboardContext.model_rebuild()
|
||||
MaxInsightContext.model_rebuild()
|
||||
QueryRequest.model_rebuild()
|
||||
|
||||