Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions src/dispatch/entity_type/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ def get_all(*, db_session: Session, scope: str = None) -> Query:
return db_session.query(EntityType)


def create(*, db_session: Session, entity_type_in: EntityTypeCreate) -> EntityType:
"""Creates a new entity type."""
def create(
*, db_session: Session, entity_type_in: EntityTypeCreate, case_id: int | None = None
) -> EntityType:
"""Creates a new entity type and extracts entities from existing signal instances."""
project = project_service.get_by_name_or_raise(
db_session=db_session, project_in=entity_type_in.project
)
Expand All @@ -75,10 +77,35 @@ def create(*, db_session: Session, entity_type_in: EntityTypeCreate) -> EntityTy

db_session.add(entity_type)
db_session.commit()

# Extract entities for all relevant signal instances
from dispatch.signal.models import SignalInstance
from dispatch.entity.service import find_entities

if case_id:
# Get all signal instances for the case
signal_instances = (
db_session.query(SignalInstance)
.filter(SignalInstance.case_id == case_id)
.limit(100)
.all()
)
# Extract and create entities for these instances using only the new entity_type
for signal_instance in signal_instances:
new_entities = find_entities(db_session, signal_instance, [entity_type])
# Associate new entities with the signal_instance
for entity in new_entities:
if entity not in signal_instance.entities:
signal_instance.entities.append(entity)

db_session.commit()

return entity_type


def get_or_create(*, db_session: Session, entity_type_in: EntityTypeCreate) -> EntityType:
def get_or_create(
*, db_session: Session, entity_type_in: EntityTypeCreate, case_id: int | None = None
) -> EntityType:
"""Gets or creates a new entity type."""
q = (
db_session.query(EntityType)
Expand All @@ -90,7 +117,7 @@ def get_or_create(*, db_session: Session, entity_type_in: EntityTypeCreate) -> E
if instance:
return instance

return create(db_session=db_session, entity_type_in=entity_type_in)
return create(db_session=db_session, entity_type_in=entity_type_in, case_id=case_id)


def update(
Expand Down
58 changes: 41 additions & 17 deletions src/dispatch/entity_type/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import Union
from typing import List

from fastapi import APIRouter, HTTPException, status
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from sqlalchemy.exc import IntegrityError

from dispatch.case.service import get as get_case
from dispatch.database.core import DbSession
from dispatch.exceptions import ExistsError
from dispatch.database.service import CommonParameters, search_filter_sort_paginate
from dispatch.models import PrimaryKey
from dispatch.signal.service import get_signal_instance
from dispatch.signal.models import SignalInstanceRead

from .models import (
Expand Down Expand Up @@ -54,11 +54,24 @@ def create_entity_type(db_session: DbSession, entity_type_in: EntityTypeCreate):
return entity_type


@router.put("/recalculate/{entity_type_id}/{signal_instance_id}", response_model=SignalInstanceRead)
def recalculate(
db_session: DbSession, entity_type_id: PrimaryKey, signal_instance_id: Union[str, PrimaryKey]
@router.post("/{case_id}", response_model=EntityTypeRead)
def create_entity_type_with_case(
db_session: DbSession, case_id: PrimaryKey, entity_type_in: EntityTypeCreate
):
"""Recalculates the associated entities for a signal instance."""
"""Create a new entity."""
try:
entity_type = create(db_session=db_session, entity_type_in=entity_type_in, case_id=case_id)
except IntegrityError:
raise ValidationError(
[ErrorWrapper(ExistsError(msg="An entity with this name already exists."), loc="name")],
model=EntityTypeCreate,
) from None
return entity_type


@router.put("/recalculate/{entity_type_id}/{case_id}", response_model=List[SignalInstanceRead])
def recalculate(db_session: DbSession, entity_type_id: PrimaryKey, case_id: PrimaryKey):
"""Recalculates the associated entities for all signal instances in a case."""
entity_type = get(
db_session=db_session,
entity_type_id=entity_type_id,
Expand All @@ -69,21 +82,32 @@ def recalculate(
detail=[{"msg": "An entity type with this id does not exist."}],
)

signal_instance = get_signal_instance(
db_session=db_session,
signal_instance_id=signal_instance_id,
)
if not signal_instance:
case = get_case(db_session=db_session, case_id=case_id)
if not case:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=[{"msg": "A signal instance with this id does not exist."}],
detail=[{"msg": "A case with this id does not exist."}],
)

return recalculate_entity_flow(
db_session=db_session,
entity_type=entity_type,
signal_instance=signal_instance,
)
# Get all signal instances associated with the case
signal_instances = case.signal_instances
if not signal_instances:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=[{"msg": "No signal instances found for this case."}],
)

# Recalculate entities for each signal instance
updated_signal_instances = []
for signal_instance in signal_instances:
updated_signal_instance = recalculate_entity_flow(
db_session=db_session,
entity_type=entity_type,
signal_instance=signal_instance,
)
updated_signal_instances.append(updated_signal_instance)

return updated_signal_instances


@router.put("/{entity_type_id}", response_model=EntityTypeRead)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
:options="editorOptions"
:editorMounted="editorMounted"
language="json"
style="width: 100%; height: 100%"
style="width: 100%; height: 240px"
/>
</div>

Expand Down Expand Up @@ -330,7 +330,12 @@ const saveEntityType = async () => {
signals: [signalGetResponse.data],
}
try {
const newEntityType = await EntityTypeApi.create(entityTypeData)
const newEntityType = await EntityTypeApi.create_with_case(
entityTypeData,
selectedCase.value.id
)
// Use the case_id instead of signal_instance_id for recalculation
await EntityTypeApi.recalculate(newEntityType.data.id, selectedCase.value.id)
emit("new-entity-type", newEntityType.data)
store.commit(
"notification_backend/addBeNotification",
Expand All @@ -340,7 +345,6 @@ const saveEntityType = async () => {
},
{ root: true }
)
await EntityTypeApi.recalculate(newEntityType.data.id, props.signalObj.raw.id)
} catch (error) {
store.commit(
"notification_backend/addBeNotification",
Expand Down
8 changes: 6 additions & 2 deletions src/dispatch/static/dispatch/src/entity_type/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export default {
return API.post(`${resource}`, payload)
},

create_with_case(payload, caseId) {
return API.post(`${resource}/${caseId}`, payload)
},

update(entityTypeId, payload) {
return API.put(`${resource}/${entityTypeId}`, payload)
},
Expand All @@ -27,7 +31,7 @@ export default {
return API.delete(`${resource}/${entityTypeId}`)
},

recalculate(entityTypeId, signalInstanceId) {
return API.put(`${resource}/recalculate/${entityTypeId}/${signalInstanceId}`)
recalculate(entityTypeId, caseId) {
return API.put(`${resource}/recalculate/${entityTypeId}/${caseId}`)
},
}
112 changes: 93 additions & 19 deletions src/dispatch/static/dispatch/src/signal/NewRawSignalViewer.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup>
import { computed, ref, watch } from "vue"
import { computed, ref, watch, onBeforeUnmount } from "vue"

// Necessary import for JSON language server
// eslint-disable-next-line no-unused-vars
Expand Down Expand Up @@ -33,25 +33,82 @@ const newEntityTypeJpath = ref("")

const raw_str = computed(() => JSON.stringify(props.value.raw, null, "\t") || "[]")

// Store a reference to the hover provider for cleanup
let hoverProviderDisposable = null

// Clean up function to dispose of hover providers
const cleanupHoverProviders = (monacoInstance) => {
if (!monacoInstance || !monacoInstance.languages) return

// Get all registered hover providers for JSON language
const providers = monacoInstance.languages.getHoverProviders
? monacoInstance.languages.getHoverProviders("json")
: []

// Dispose all existing hover providers
for (const provider of providers) {
provider.dispose()
}

// Also dispose our tracked provider if it exists
if (hoverProviderDisposable) {
hoverProviderDisposable.dispose()
hoverProviderDisposable = null
}
}

// Ensure cleanup when component is unmounted
onBeforeUnmount(() => {
// We'll use the global monaco instance if available
if (typeof monaco !== "undefined") {
cleanupHoverProviders(monaco)
}
})

const editorBeforeMount = (monaco) => {
monaco.languages.registerHoverProvider("json", {
// Clean up any existing hover providers
cleanupHoverProviders(monaco)

// Register a single new hover provider
hoverProviderDisposable = monaco.languages.registerHoverProvider("json", {
provideHover: (model, position) => {
const word = model.getWordAtPosition(position)

if (word) {
const hoveredWord = word.word

const entity = props.value.entities.find((entity) => entity.value === hoveredWord)
if (entity) {
// Check in existing entities first
const existingEntity = props.value.entities.find((entity) => entity.value === hoveredWord)
if (existingEntity) {
const range = new monaco.Range(
position.lineNumber,
word.startColumn,
position.lineNumber,
word.endColumn
)
const title = `**Entity Type**: ${entity.entity_type.name}\n`
const pattern = `**Pattern**: ${entity.entity_type.jpath}\n`
const value = `**Value**: ${entity.value}`
const title = `**Entity Type**: ${existingEntity.entity_type.name}\n`
const pattern = `**Pattern**: ${existingEntity.entity_type.jpath}\n`
const value = `**Value**: ${existingEntity.value}`
return {
contents: [{ value: title }, { value: pattern }, { value: value }],
range: range,
}
}

// Then check in new entities
const newEntity = newEntities.value.find((entity) => entity.value === hoveredWord)
if (newEntity) {
const range = new monaco.Range(
position.lineNumber,
word.startColumn,
position.lineNumber,
word.endColumn
)
// Handle different entity structures
const entityType = newEntity.entity_type || newEntity
const title = `**Entity Type**: ${entityType.name}\n`
const pattern = `**Pattern**: ${entityType.jpath}\n`
const value = `**Value**: ${newEntity.value}`
return {
contents: [{ value: title }, { value: pattern }, { value: value }],
range: range,
Expand Down Expand Up @@ -113,6 +170,9 @@ const editorMounted = (editor, monaco) => {

glyphLines.clear()

// Combine existing and new entities for checking
const allEntities = [...props.value.entities, ...newEntities.value]

function traverse(node) {
// Traverse `Object` and `Array` nodes to reach their child `Property` nodes.
if (node.type === "Object" || node.type === "Array") {
Expand All @@ -122,19 +182,28 @@ const editorMounted = (editor, monaco) => {
const { key, value } = node
// Check for `Literal` type and string value.
if (value.type === "Literal" && typeof value.value === "string") {
const start = model.getPositionAt(value.loc.start.offset)
const end = model.getPositionAt(value.loc.end.offset)

decorations.push({
range: new monaco.Range(start.lineNumber, 1, end.lineNumber, 1),
options: {
isWholeLine: true,
glyphMarginClassName: "myGlyphMarginClass",
glyphMarginHoverMessage: { value: `Extract new entity from "**${key.value}**"` },
},
// Skip adding glyph if this value already exists as an entity
const stringValue = value.value
const entityExists = allEntities.some((entity) => {
// Handle both entity objects and entity_type objects
return entity.value === stringValue
})
// Record line numbers where glyphs were added.
glyphLines.add(start.lineNumber)

if (!entityExists) {
const start = model.getPositionAt(value.loc.start.offset)
const end = model.getPositionAt(value.loc.end.offset)

decorations.push({
range: new monaco.Range(start.lineNumber, 1, end.lineNumber, 1),
options: {
isWholeLine: true,
glyphMarginClassName: "myGlyphMarginClass",
glyphMarginHoverMessage: [{ value: `Extract new entity from "**${key.value}**"` }],
},
})
// Record line numbers where glyphs were added.
glyphLines.add(start.lineNumber)
}
}

// If the value of the property is an object or array, traverse it as well
Expand Down Expand Up @@ -225,6 +294,11 @@ const editorOptions = {
wordWrap: true,
glyphMargin: true,
contextmenu: false,
// Configure hover settings for immediate response
hover: {
delay: 0, // Show hover immediately with no delay
sticky: true, // Make hovers sticky so they don't disappear quickly
},
scrollbar: {
vertical: "hidden",
},
Expand Down
3 changes: 1 addition & 2 deletions src/dispatch/static/dispatch/src/util/jpath.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import jsonpath from "jsonpath"
import json_to_ast from "json-to-ast"

/**
* Finds the path to a key in an object hierarchy.
Expand All @@ -24,7 +23,7 @@ import json_to_ast from "json-to-ast"
export function findPath<T>(obj: T, key: keyof any, value: any, path: string = "$"): string | null {
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
const arrayPath = `${path}[${i}]`
const arrayPath = `${path}[*]`
const result = findPath(obj[i], key, value, arrayPath)
if (result) return result
}
Expand Down
Loading