orchestrator-core 4.6.2__py3-none-any.whl → 4.6.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- orchestrator/__init__.py +1 -1
- orchestrator/api/api_v1/endpoints/search.py +44 -34
- orchestrator/{search/retrieval/utils.py → cli/search/display.py} +4 -29
- orchestrator/cli/search/search_explore.py +22 -24
- orchestrator/cli/search/speedtest.py +11 -9
- orchestrator/db/models.py +6 -6
- orchestrator/graphql/resolvers/helpers.py +15 -0
- orchestrator/graphql/resolvers/process.py +5 -3
- orchestrator/graphql/resolvers/product.py +3 -2
- orchestrator/graphql/resolvers/product_block.py +3 -2
- orchestrator/graphql/resolvers/resource_type.py +3 -2
- orchestrator/graphql/resolvers/scheduled_tasks.py +3 -1
- orchestrator/graphql/resolvers/settings.py +2 -0
- orchestrator/graphql/resolvers/subscription.py +5 -3
- orchestrator/graphql/resolvers/version.py +2 -0
- orchestrator/graphql/resolvers/workflow.py +3 -2
- orchestrator/log_config.py +2 -0
- orchestrator/schemas/search.py +1 -1
- orchestrator/schemas/search_requests.py +59 -0
- orchestrator/search/agent/handlers.py +129 -0
- orchestrator/search/agent/prompts.py +54 -33
- orchestrator/search/agent/state.py +9 -24
- orchestrator/search/agent/tools.py +223 -144
- orchestrator/search/agent/validation.py +80 -0
- orchestrator/search/{schemas → aggregations}/__init__.py +20 -0
- orchestrator/search/aggregations/base.py +201 -0
- orchestrator/search/core/types.py +3 -2
- orchestrator/search/filters/__init__.py +4 -0
- orchestrator/search/filters/definitions.py +22 -1
- orchestrator/search/filters/numeric_filter.py +3 -3
- orchestrator/search/llm_migration.py +2 -1
- orchestrator/search/query/__init__.py +90 -0
- orchestrator/search/query/builder.py +285 -0
- orchestrator/search/query/engine.py +162 -0
- orchestrator/search/{retrieval → query}/exceptions.py +38 -7
- orchestrator/search/query/mixins.py +95 -0
- orchestrator/search/query/queries.py +129 -0
- orchestrator/search/query/results.py +252 -0
- orchestrator/search/{retrieval/query_state.py → query/state.py} +31 -11
- orchestrator/search/{retrieval → query}/validation.py +58 -1
- orchestrator/search/retrieval/__init__.py +0 -5
- orchestrator/search/retrieval/pagination.py +7 -8
- orchestrator/search/retrieval/retrievers/base.py +9 -9
- orchestrator/workflows/translations/en-GB.json +1 -0
- {orchestrator_core-4.6.2.dist-info → orchestrator_core-4.6.3.dist-info}/METADATA +16 -15
- {orchestrator_core-4.6.2.dist-info → orchestrator_core-4.6.3.dist-info}/RECORD +49 -43
- orchestrator/search/retrieval/builder.py +0 -127
- orchestrator/search/retrieval/engine.py +0 -197
- orchestrator/search/schemas/parameters.py +0 -133
- orchestrator/search/schemas/results.py +0 -80
- /orchestrator/search/{export.py → query/export.py} +0 -0
- {orchestrator_core-4.6.2.dist-info → orchestrator_core-4.6.3.dist-info}/WHEEL +0 -0
- {orchestrator_core-4.6.2.dist-info → orchestrator_core-4.6.3.dist-info}/licenses/LICENSE +0 -0
orchestrator/__init__.py
CHANGED
|
@@ -21,35 +21,32 @@ from orchestrator.schemas.search import (
|
|
|
21
21
|
PathsResponse,
|
|
22
22
|
SearchResultsSchema,
|
|
23
23
|
)
|
|
24
|
+
from orchestrator.schemas.search_requests import SearchRequest
|
|
24
25
|
from orchestrator.search.core.exceptions import InvalidCursorError, QueryStateNotFoundError
|
|
25
26
|
from orchestrator.search.core.types import EntityType, UIType
|
|
26
|
-
from orchestrator.search.filters.definitions import generate_definitions
|
|
27
|
-
from orchestrator.search.
|
|
28
|
-
from orchestrator.search.
|
|
27
|
+
from orchestrator.search.filters.definitions import TypeDefinition, generate_definitions
|
|
28
|
+
from orchestrator.search.query import QueryState, engine
|
|
29
|
+
from orchestrator.search.query.builder import build_paths_query, create_path_autocomplete_lquery, process_path_rows
|
|
30
|
+
from orchestrator.search.query.queries import ExportQuery, SelectQuery
|
|
31
|
+
from orchestrator.search.query.results import SearchResult
|
|
32
|
+
from orchestrator.search.query.validation import is_lquery_syntactically_valid
|
|
29
33
|
from orchestrator.search.retrieval.pagination import PageCursor, encode_next_page_cursor
|
|
30
|
-
from orchestrator.search.retrieval.validation import is_lquery_syntactically_valid
|
|
31
|
-
from orchestrator.search.schemas.parameters import (
|
|
32
|
-
ProcessSearchParameters,
|
|
33
|
-
ProductSearchParameters,
|
|
34
|
-
SearchParameters,
|
|
35
|
-
SubscriptionSearchParameters,
|
|
36
|
-
WorkflowSearchParameters,
|
|
37
|
-
)
|
|
38
|
-
from orchestrator.search.schemas.results import SearchResult, TypeDefinition
|
|
39
34
|
|
|
40
35
|
router = APIRouter()
|
|
41
36
|
logger = structlog.get_logger(__name__)
|
|
42
37
|
|
|
43
38
|
|
|
44
39
|
async def _perform_search_and_fetch(
|
|
45
|
-
|
|
40
|
+
entity_type: EntityType | None = None,
|
|
41
|
+
request: SearchRequest | None = None,
|
|
46
42
|
cursor: str | None = None,
|
|
47
43
|
query_id: str | None = None,
|
|
48
44
|
) -> SearchResultsSchema[SearchResult]:
|
|
49
45
|
"""Execute search with optional pagination.
|
|
50
46
|
|
|
51
47
|
Args:
|
|
52
|
-
|
|
48
|
+
entity_type: Entity type to search
|
|
49
|
+
request: Search request for new search
|
|
53
50
|
cursor: Pagination cursor (loads saved query state)
|
|
54
51
|
query_id: Saved query ID to retrieve and execute
|
|
55
52
|
|
|
@@ -58,27 +55,31 @@ async def _perform_search_and_fetch(
|
|
|
58
55
|
"""
|
|
59
56
|
try:
|
|
60
57
|
page_cursor: PageCursor | None = None
|
|
58
|
+
query: SelectQuery
|
|
61
59
|
|
|
62
60
|
if cursor:
|
|
63
61
|
page_cursor = PageCursor.decode(cursor)
|
|
64
|
-
query_state =
|
|
62
|
+
query_state = QueryState.load_from_id(page_cursor.query_id, SelectQuery)
|
|
63
|
+
query = query_state.query
|
|
64
|
+
|
|
65
65
|
elif query_id:
|
|
66
|
-
query_state =
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
query_state = QueryState.load_from_id(query_id, SelectQuery)
|
|
67
|
+
query = query_state.query
|
|
68
|
+
|
|
69
|
+
elif request and entity_type:
|
|
70
|
+
query = request.to_query(entity_type)
|
|
71
|
+
query_state = QueryState(query=query, query_embedding=None)
|
|
69
72
|
else:
|
|
70
73
|
raise HTTPException(
|
|
71
74
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
72
|
-
detail="Either
|
|
75
|
+
detail="Either (request + entity_type), cursor, or query_id must be provided",
|
|
73
76
|
)
|
|
74
77
|
|
|
75
|
-
search_response = await execute_search(
|
|
76
|
-
query_state.parameters, db.session, page_cursor, query_state.query_embedding
|
|
77
|
-
)
|
|
78
|
+
search_response = await engine.execute_search(query, db.session, page_cursor, query_state.query_embedding)
|
|
78
79
|
if not search_response.results:
|
|
79
80
|
return SearchResultsSchema(search_metadata=search_response.metadata)
|
|
80
81
|
|
|
81
|
-
next_page_cursor = encode_next_page_cursor(search_response, page_cursor,
|
|
82
|
+
next_page_cursor = encode_next_page_cursor(search_response, page_cursor, query)
|
|
82
83
|
has_next_page = next_page_cursor is not None
|
|
83
84
|
page_info = PageInfoSchema(has_next_page=has_next_page, next_page_cursor=next_page_cursor)
|
|
84
85
|
|
|
@@ -98,34 +99,34 @@ async def _perform_search_and_fetch(
|
|
|
98
99
|
|
|
99
100
|
@router.post("/subscriptions", response_model=SearchResultsSchema[SearchResult])
|
|
100
101
|
async def search_subscriptions(
|
|
101
|
-
|
|
102
|
+
request: SearchRequest,
|
|
102
103
|
cursor: str | None = None,
|
|
103
104
|
) -> SearchResultsSchema[SearchResult]:
|
|
104
|
-
return await _perform_search_and_fetch(
|
|
105
|
+
return await _perform_search_and_fetch(EntityType.SUBSCRIPTION, request, cursor)
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
@router.post("/workflows", response_model=SearchResultsSchema[SearchResult])
|
|
108
109
|
async def search_workflows(
|
|
109
|
-
|
|
110
|
+
request: SearchRequest,
|
|
110
111
|
cursor: str | None = None,
|
|
111
112
|
) -> SearchResultsSchema[SearchResult]:
|
|
112
|
-
return await _perform_search_and_fetch(
|
|
113
|
+
return await _perform_search_and_fetch(EntityType.WORKFLOW, request, cursor)
|
|
113
114
|
|
|
114
115
|
|
|
115
116
|
@router.post("/products", response_model=SearchResultsSchema[SearchResult])
|
|
116
117
|
async def search_products(
|
|
117
|
-
|
|
118
|
+
request: SearchRequest,
|
|
118
119
|
cursor: str | None = None,
|
|
119
120
|
) -> SearchResultsSchema[SearchResult]:
|
|
120
|
-
return await _perform_search_and_fetch(
|
|
121
|
+
return await _perform_search_and_fetch(EntityType.PRODUCT, request, cursor)
|
|
121
122
|
|
|
122
123
|
|
|
123
124
|
@router.post("/processes", response_model=SearchResultsSchema[SearchResult])
|
|
124
125
|
async def search_processes(
|
|
125
|
-
|
|
126
|
+
request: SearchRequest,
|
|
126
127
|
cursor: str | None = None,
|
|
127
128
|
) -> SearchResultsSchema[SearchResult]:
|
|
128
|
-
return await _perform_search_and_fetch(
|
|
129
|
+
return await _perform_search_and_fetch(EntityType.PROCESS, request, cursor)
|
|
129
130
|
|
|
130
131
|
|
|
131
132
|
@router.get(
|
|
@@ -191,7 +192,7 @@ async def export_by_query_id(query_id: str) -> ExportResponse:
|
|
|
191
192
|
as flattened records suitable for CSV download.
|
|
192
193
|
|
|
193
194
|
Args:
|
|
194
|
-
query_id:
|
|
195
|
+
query_id: QueryTypes UUID
|
|
195
196
|
|
|
196
197
|
Returns:
|
|
197
198
|
ExportResponse containing 'page' with an array of flattened entity records.
|
|
@@ -200,8 +201,17 @@ async def export_by_query_id(query_id: str) -> ExportResponse:
|
|
|
200
201
|
HTTPException: 404 if query not found, 400 if invalid data
|
|
201
202
|
"""
|
|
202
203
|
try:
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
# Load SelectQuery from the database (what gets saved during search)
|
|
205
|
+
query_state = QueryState.load_from_id(query_id, SelectQuery)
|
|
206
|
+
|
|
207
|
+
# Convert to ExportQuery with export-appropriate limit
|
|
208
|
+
export_query = ExportQuery(
|
|
209
|
+
entity_type=query_state.query.entity_type,
|
|
210
|
+
filters=query_state.query.filters,
|
|
211
|
+
query_text=query_state.query.query_text,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
export_records = await engine.execute_export(export_query, db.session, query_state.query_embedding)
|
|
205
215
|
return ExportResponse(page=export_records)
|
|
206
216
|
except ValueError as e:
|
|
207
217
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
|
-
import re
|
|
16
15
|
|
|
17
16
|
import structlog
|
|
18
17
|
from sqlalchemy import and_
|
|
@@ -22,37 +21,13 @@ from orchestrator.db.database import WrappedSession
|
|
|
22
21
|
from orchestrator.db.models import AiSearchIndex
|
|
23
22
|
from orchestrator.search.core.types import EntityType
|
|
24
23
|
from orchestrator.search.indexing.registry import ENTITY_CONFIG_REGISTRY
|
|
25
|
-
from orchestrator.search.
|
|
26
|
-
from orchestrator.search.
|
|
24
|
+
from orchestrator.search.query.queries import BaseQuery
|
|
25
|
+
from orchestrator.search.query.results import SearchResult
|
|
27
26
|
|
|
28
27
|
logger = structlog.get_logger(__name__)
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
def
|
|
32
|
-
"""Finds all occurrences of individual words from the term, including both word boundary and substring matches."""
|
|
33
|
-
if not text or not term:
|
|
34
|
-
return []
|
|
35
|
-
|
|
36
|
-
all_matches = []
|
|
37
|
-
words = [w.strip() for w in term.split() if w.strip()]
|
|
38
|
-
|
|
39
|
-
for word in words:
|
|
40
|
-
# First find word boundary matches
|
|
41
|
-
word_boundary_pattern = rf"\b{re.escape(word)}\b"
|
|
42
|
-
word_matches = list(re.finditer(word_boundary_pattern, text, re.IGNORECASE))
|
|
43
|
-
all_matches.extend([(m.start(), m.end()) for m in word_matches])
|
|
44
|
-
|
|
45
|
-
# Then find all substring matches
|
|
46
|
-
substring_pattern = re.escape(word)
|
|
47
|
-
substring_matches = list(re.finditer(substring_pattern, text, re.IGNORECASE))
|
|
48
|
-
all_matches.extend([(m.start(), m.end()) for m in substring_matches])
|
|
49
|
-
|
|
50
|
-
return sorted(set(all_matches))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def display_filtered_paths_only(
|
|
54
|
-
results: list[SearchResult], search_params: BaseSearchParameters, db_session: WrappedSession
|
|
55
|
-
) -> None:
|
|
30
|
+
def display_filtered_paths_only(results: list[SearchResult], query: BaseQuery, db_session: WrappedSession) -> None:
|
|
56
31
|
"""Display only the paths that were searched for in the results."""
|
|
57
32
|
if not results:
|
|
58
33
|
logger.info("No results found.")
|
|
@@ -60,7 +35,7 @@ def display_filtered_paths_only(
|
|
|
60
35
|
|
|
61
36
|
logger.info("--- Search Results ---")
|
|
62
37
|
|
|
63
|
-
searched_paths =
|
|
38
|
+
searched_paths = query.filters.get_all_paths() if query.filters else []
|
|
64
39
|
if not searched_paths:
|
|
65
40
|
return
|
|
66
41
|
|
|
@@ -4,13 +4,13 @@ import structlog
|
|
|
4
4
|
import typer
|
|
5
5
|
from pydantic import ValidationError
|
|
6
6
|
|
|
7
|
+
from orchestrator.cli.search.display import display_filtered_paths_only, display_results
|
|
7
8
|
from orchestrator.db import db
|
|
8
9
|
from orchestrator.search.core.types import EntityType, FilterOp, UIType
|
|
9
10
|
from orchestrator.search.filters import EqualityFilter, FilterTree, LtreeFilter, PathFilter
|
|
10
|
-
from orchestrator.search.
|
|
11
|
-
from orchestrator.search.
|
|
12
|
-
from orchestrator.search.
|
|
13
|
-
from orchestrator.search.schemas.parameters import BaseSearchParameters
|
|
11
|
+
from orchestrator.search.query import engine
|
|
12
|
+
from orchestrator.search.query.queries import SelectQuery
|
|
13
|
+
from orchestrator.search.query.validation import get_structured_filter_schema
|
|
14
14
|
|
|
15
15
|
app = typer.Typer(help="Experiment with the subscription search indexes.")
|
|
16
16
|
|
|
@@ -31,16 +31,14 @@ def structured(path: str, value: str, entity_type: EntityType = EntityType.SUBSC
|
|
|
31
31
|
...
|
|
32
32
|
"""
|
|
33
33
|
path_filter = PathFilter(path=path, condition=EqualityFilter(op=FilterOp.EQ, value=value), value_kind=UIType.STRING)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
search_response = asyncio.run(execute_search(search_params=search_params, db_session=db.session))
|
|
38
|
-
display_filtered_paths_only(search_response.results, search_params, db.session)
|
|
34
|
+
query = SelectQuery(entity_type=entity_type, filters=FilterTree.from_flat_and([path_filter]), limit=limit)
|
|
35
|
+
search_response = asyncio.run(engine.execute_search(query=query, db_session=db.session))
|
|
36
|
+
display_filtered_paths_only(search_response.results, query, db.session)
|
|
39
37
|
display_results(search_response.results, db.session, "Match")
|
|
40
38
|
|
|
41
39
|
|
|
42
40
|
@app.command()
|
|
43
|
-
def semantic(
|
|
41
|
+
def semantic(query_text: str, entity_type: EntityType = EntityType.SUBSCRIPTION, limit: int = 10) -> None:
|
|
44
42
|
"""Finds subscriptions that are conceptually most similar to the query text.
|
|
45
43
|
|
|
46
44
|
Example:
|
|
@@ -52,8 +50,8 @@ def semantic(query: str, entity_type: EntityType = EntityType.SUBSCRIPTION, limi
|
|
|
52
50
|
},
|
|
53
51
|
...
|
|
54
52
|
"""
|
|
55
|
-
|
|
56
|
-
search_response = asyncio.run(execute_search(
|
|
53
|
+
query = SelectQuery(entity_type=entity_type, query_text=query_text, limit=limit)
|
|
54
|
+
search_response = asyncio.run(engine.execute_search(query=query, db_session=db.session))
|
|
57
55
|
display_results(search_response.results, db.session, "Distance")
|
|
58
56
|
|
|
59
57
|
|
|
@@ -70,8 +68,8 @@ def fuzzy(term: str, entity_type: EntityType = EntityType.SUBSCRIPTION, limit: i
|
|
|
70
68
|
},
|
|
71
69
|
...
|
|
72
70
|
"""
|
|
73
|
-
|
|
74
|
-
search_response = asyncio.run(execute_search(
|
|
71
|
+
query = SelectQuery(entity_type=entity_type, query_text=term, limit=limit)
|
|
72
|
+
search_response = asyncio.run(engine.execute_search(query=query, db_session=db.session))
|
|
75
73
|
display_results(search_response.results, db.session, "Similarity")
|
|
76
74
|
|
|
77
75
|
|
|
@@ -79,7 +77,7 @@ def fuzzy(term: str, entity_type: EntityType = EntityType.SUBSCRIPTION, limit: i
|
|
|
79
77
|
def hierarchical(
|
|
80
78
|
op: str = typer.Argument(..., help="The hierarchical operation to perform."),
|
|
81
79
|
path: str = typer.Argument(..., help="The ltree path or lquery pattern for the operation."),
|
|
82
|
-
|
|
80
|
+
query_text: str | None = typer.Option(None, "--query", "-q", help="An optional fuzzy term to rank the results."),
|
|
83
81
|
entity_type: EntityType = EntityType.SUBSCRIPTION,
|
|
84
82
|
limit: int = 10,
|
|
85
83
|
) -> None:
|
|
@@ -96,23 +94,23 @@ def hierarchical(
|
|
|
96
94
|
|
|
97
95
|
path_filter = PathFilter(path="ltree_hierarchical_filter", condition=condition, value_kind=UIType.STRING)
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
entity_type=entity_type, filters=FilterTree.from_flat_and([path_filter]),
|
|
97
|
+
query = SelectQuery(
|
|
98
|
+
entity_type=entity_type, filters=FilterTree.from_flat_and([path_filter]), query_text=query_text, limit=limit
|
|
101
99
|
)
|
|
102
|
-
search_response = asyncio.run(execute_search(
|
|
100
|
+
search_response = asyncio.run(engine.execute_search(query=query, db_session=db.session))
|
|
103
101
|
display_results(search_response.results, db.session, "Hierarchical Score")
|
|
104
102
|
|
|
105
103
|
|
|
106
104
|
@app.command()
|
|
107
|
-
def hybrid(
|
|
105
|
+
def hybrid(query_text: str, term: str, entity_type: EntityType = EntityType.SUBSCRIPTION, limit: int = 10) -> None:
|
|
108
106
|
"""Performs a hybrid search, combining semantic and fuzzy matching.
|
|
109
107
|
|
|
110
108
|
Example:
|
|
111
109
|
dotenv run python main.py search hybrid "reptile store" "Kingswood"
|
|
112
110
|
"""
|
|
113
|
-
|
|
114
|
-
logger.info("Executing Hybrid Search",
|
|
115
|
-
search_response = asyncio.run(execute_search(
|
|
111
|
+
query = SelectQuery(entity_type=entity_type, query_text=query_text, limit=limit)
|
|
112
|
+
logger.info("Executing Hybrid Search", query_text=query_text, term=term)
|
|
113
|
+
search_response = asyncio.run(engine.execute_search(query=query, db_session=db.session))
|
|
116
114
|
display_results(search_response.results, db.session, "Hybrid Score")
|
|
117
115
|
|
|
118
116
|
|
|
@@ -198,8 +196,8 @@ def nested_demo(entity_type: EntityType = EntityType.SUBSCRIPTION, limit: int =
|
|
|
198
196
|
}
|
|
199
197
|
)
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
search_response = asyncio.run(execute_search(
|
|
199
|
+
query = SelectQuery(entity_type=entity_type, filters=tree, limit=limit)
|
|
200
|
+
search_response = asyncio.run(engine.execute_search(query=query, db_session=db.session))
|
|
203
201
|
|
|
204
202
|
display_results(search_response.results, db.session, "Score")
|
|
205
203
|
|
|
@@ -12,8 +12,8 @@ from orchestrator.db import db
|
|
|
12
12
|
from orchestrator.search.core.embedding import QueryEmbedder
|
|
13
13
|
from orchestrator.search.core.types import EntityType
|
|
14
14
|
from orchestrator.search.core.validators import is_uuid
|
|
15
|
-
from orchestrator.search.
|
|
16
|
-
from orchestrator.search.
|
|
15
|
+
from orchestrator.search.query import engine
|
|
16
|
+
from orchestrator.search.query.queries import SelectQuery
|
|
17
17
|
|
|
18
18
|
logger = structlog.get_logger(__name__)
|
|
19
19
|
console = Console()
|
|
@@ -50,23 +50,25 @@ async def generate_embeddings_for_queries(queries: list[str]) -> dict[str, list[
|
|
|
50
50
|
return embedding_lookup
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
async def run_single_query(
|
|
54
|
-
|
|
53
|
+
async def run_single_query(query_text: str, embedding_lookup: dict[str, list[float]]) -> dict[str, Any]:
|
|
54
|
+
query = SelectQuery(entity_type=EntityType.SUBSCRIPTION, query_text=query_text, limit=30)
|
|
55
55
|
|
|
56
56
|
query_embedding = None
|
|
57
57
|
|
|
58
|
-
if is_uuid(
|
|
59
|
-
logger.debug("Using fuzzy-only ranking for full UUID",
|
|
58
|
+
if is_uuid(query_text):
|
|
59
|
+
logger.debug("Using fuzzy-only ranking for full UUID", query_text=query_text)
|
|
60
60
|
else:
|
|
61
|
-
query_embedding = embedding_lookup[
|
|
61
|
+
query_embedding = embedding_lookup[query_text]
|
|
62
62
|
|
|
63
63
|
with db.session as session:
|
|
64
64
|
start_time = time.perf_counter()
|
|
65
|
-
response = await execute_search(
|
|
65
|
+
response = await engine.execute_search(
|
|
66
|
+
query=query, db_session=session, cursor=None, query_embedding=query_embedding
|
|
67
|
+
)
|
|
66
68
|
end_time = time.perf_counter()
|
|
67
69
|
|
|
68
70
|
return {
|
|
69
|
-
"query":
|
|
71
|
+
"query": query_text,
|
|
70
72
|
"time": end_time - start_time,
|
|
71
73
|
"results": len(response.results),
|
|
72
74
|
"search_type": response.metadata.search_type if hasattr(response, "metadata") else "unknown",
|
orchestrator/db/models.py
CHANGED
|
@@ -60,7 +60,7 @@ from orchestrator.utils.datetime import nowtz
|
|
|
60
60
|
from orchestrator.version import GIT_COMMIT_HASH
|
|
61
61
|
|
|
62
62
|
if TYPE_CHECKING:
|
|
63
|
-
from orchestrator.search.
|
|
63
|
+
from orchestrator.search.query.state import QueryState
|
|
64
64
|
|
|
65
65
|
logger = structlog.get_logger(__name__)
|
|
66
66
|
|
|
@@ -707,7 +707,7 @@ class SearchQueryTable(BaseModel):
|
|
|
707
707
|
)
|
|
708
708
|
query_number = mapped_column(Integer, nullable=False)
|
|
709
709
|
|
|
710
|
-
# Search parameters as JSONB (maps to
|
|
710
|
+
# Search parameters as JSONB (maps to BaseQuery subclasses)
|
|
711
711
|
parameters = mapped_column(pg.JSONB, nullable=False)
|
|
712
712
|
|
|
713
713
|
# Query embedding for semantic search (pgvector)
|
|
@@ -726,14 +726,14 @@ class SearchQueryTable(BaseModel):
|
|
|
726
726
|
@classmethod
|
|
727
727
|
def from_state(
|
|
728
728
|
cls,
|
|
729
|
-
state: "
|
|
729
|
+
state: "QueryState",
|
|
730
730
|
run_id: "UUID | None" = None,
|
|
731
731
|
query_number: int = 1,
|
|
732
732
|
) -> "SearchQueryTable":
|
|
733
|
-
"""Create a SearchQueryTable instance from a
|
|
733
|
+
"""Create a SearchQueryTable instance from a QueryState.
|
|
734
734
|
|
|
735
735
|
Args:
|
|
736
|
-
state:
|
|
736
|
+
state: QueryState wrapping the query and embedding
|
|
737
737
|
run_id: Optional agent run ID (NULL for regular API searches)
|
|
738
738
|
query_number: Query number within the run (default=1)
|
|
739
739
|
|
|
@@ -743,7 +743,7 @@ class SearchQueryTable(BaseModel):
|
|
|
743
743
|
return cls(
|
|
744
744
|
run_id=run_id,
|
|
745
745
|
query_number=query_number,
|
|
746
|
-
parameters=state.
|
|
746
|
+
parameters=state.query.model_dump(),
|
|
747
747
|
query_embedding=state.query_embedding,
|
|
748
748
|
)
|
|
749
749
|
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Callable, Coroutine
|
|
2
4
|
|
|
5
|
+
import structlog
|
|
3
6
|
from sqlalchemy import CompoundSelect, Select, select
|
|
4
7
|
from sqlalchemy.orm.strategy_options import _AbstractLoad
|
|
8
|
+
from starlette.concurrency import run_in_threadpool
|
|
5
9
|
|
|
6
10
|
from orchestrator.db import db
|
|
7
11
|
from orchestrator.db.database import BaseModel
|
|
8
12
|
|
|
13
|
+
logger = structlog.get_logger(__name__)
|
|
14
|
+
|
|
9
15
|
|
|
10
16
|
def rows_from_statement(
|
|
11
17
|
stmt: Select | CompoundSelect,
|
|
@@ -19,3 +25,12 @@ def rows_from_statement(
|
|
|
19
25
|
result = db.session.scalars(from_stmt)
|
|
20
26
|
uresult = result.unique() if unique else result
|
|
21
27
|
return uresult.all()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_async(f: Callable): # type: ignore
|
|
31
|
+
@wraps(f)
|
|
32
|
+
async def wrapper(*args, **kwargs) -> Coroutine: # type: ignore
|
|
33
|
+
logger.debug(f"**async, calling fn {f.__name__}")
|
|
34
|
+
return await run_in_threadpool(f, *args, **kwargs)
|
|
35
|
+
|
|
36
|
+
return wrapper
|
|
@@ -25,7 +25,7 @@ from orchestrator.db.range import apply_range_to_statement
|
|
|
25
25
|
from orchestrator.db.sorting import Sort
|
|
26
26
|
from orchestrator.db.sorting.process import process_sort_fields, sort_processes
|
|
27
27
|
from orchestrator.graphql.pagination import Connection
|
|
28
|
-
from orchestrator.graphql.resolvers.helpers import rows_from_statement
|
|
28
|
+
from orchestrator.graphql.resolvers.helpers import make_async, rows_from_statement
|
|
29
29
|
from orchestrator.graphql.schemas.process import ProcessType
|
|
30
30
|
from orchestrator.graphql.types import GraphqlFilter, GraphqlSort, OrchestratorInfo
|
|
31
31
|
from orchestrator.graphql.utils import (
|
|
@@ -55,7 +55,8 @@ def _enrich_process(process: ProcessTable, with_details: bool = False) -> Proces
|
|
|
55
55
|
return ProcessSchema(**process_data)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
@make_async
|
|
59
|
+
def resolve_process(info: OrchestratorInfo, process_id: UUID) -> ProcessType | None:
|
|
59
60
|
query_loaders = get_query_loaders_for_gql_fields(ProcessTable, info)
|
|
60
61
|
stmt = select(ProcessTable).options(*query_loaders).where(ProcessTable.process_id == process_id)
|
|
61
62
|
if process := db.session.scalar(stmt):
|
|
@@ -64,7 +65,8 @@ async def resolve_process(info: OrchestratorInfo, process_id: UUID) -> ProcessTy
|
|
|
64
65
|
return None
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
@make_async
|
|
69
|
+
def resolve_processes(
|
|
68
70
|
info: OrchestratorInfo,
|
|
69
71
|
filter_by: list[GraphqlFilter] | None = None,
|
|
70
72
|
sort_by: list[GraphqlSort] | None = None,
|
|
@@ -9,7 +9,7 @@ from orchestrator.db.range.range import apply_range_to_statement
|
|
|
9
9
|
from orchestrator.db.sorting import Sort
|
|
10
10
|
from orchestrator.db.sorting.product import product_sort_fields, sort_products
|
|
11
11
|
from orchestrator.graphql.pagination import Connection
|
|
12
|
-
from orchestrator.graphql.resolvers.helpers import rows_from_statement
|
|
12
|
+
from orchestrator.graphql.resolvers.helpers import make_async, rows_from_statement
|
|
13
13
|
from orchestrator.graphql.schemas.product import ProductType
|
|
14
14
|
from orchestrator.graphql.types import GraphqlFilter, GraphqlSort, OrchestratorInfo
|
|
15
15
|
from orchestrator.graphql.utils import create_resolver_error_handler, is_querying_page_data, to_graphql_result_page
|
|
@@ -19,7 +19,8 @@ from orchestrator.utils.search_query import create_sqlalchemy_select
|
|
|
19
19
|
logger = structlog.get_logger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
@make_async
|
|
23
|
+
def resolve_products(
|
|
23
24
|
info: OrchestratorInfo,
|
|
24
25
|
filter_by: list[GraphqlFilter] | None = None,
|
|
25
26
|
sort_by: list[GraphqlSort] | None = None,
|
|
@@ -13,7 +13,7 @@ from orchestrator.db.range.range import apply_range_to_statement
|
|
|
13
13
|
from orchestrator.db.sorting import Sort
|
|
14
14
|
from orchestrator.db.sorting.product_block import product_block_sort_fields, sort_product_blocks
|
|
15
15
|
from orchestrator.graphql.pagination import Connection
|
|
16
|
-
from orchestrator.graphql.resolvers.helpers import rows_from_statement
|
|
16
|
+
from orchestrator.graphql.resolvers.helpers import make_async, rows_from_statement
|
|
17
17
|
from orchestrator.graphql.schemas.product_block import ProductBlock
|
|
18
18
|
from orchestrator.graphql.types import GraphqlFilter, GraphqlSort, OrchestratorInfo
|
|
19
19
|
from orchestrator.graphql.utils import create_resolver_error_handler, is_querying_page_data, to_graphql_result_page
|
|
@@ -23,7 +23,8 @@ from orchestrator.utils.search_query import create_sqlalchemy_select
|
|
|
23
23
|
logger = structlog.get_logger(__name__)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
@make_async
|
|
27
|
+
def resolve_product_blocks(
|
|
27
28
|
info: OrchestratorInfo,
|
|
28
29
|
filter_by: list[GraphqlFilter] | None = None,
|
|
29
30
|
sort_by: list[GraphqlSort] | None = None,
|
|
@@ -13,7 +13,7 @@ from orchestrator.db.range import apply_range_to_statement
|
|
|
13
13
|
from orchestrator.db.sorting import Sort
|
|
14
14
|
from orchestrator.db.sorting.resource_type import resource_type_sort_fields, sort_resource_types
|
|
15
15
|
from orchestrator.graphql.pagination import Connection
|
|
16
|
-
from orchestrator.graphql.resolvers.helpers import rows_from_statement
|
|
16
|
+
from orchestrator.graphql.resolvers.helpers import make_async, rows_from_statement
|
|
17
17
|
from orchestrator.graphql.schemas.resource_type import ResourceType
|
|
18
18
|
from orchestrator.graphql.types import GraphqlFilter, GraphqlSort, OrchestratorInfo
|
|
19
19
|
from orchestrator.graphql.utils import create_resolver_error_handler, is_querying_page_data, to_graphql_result_page
|
|
@@ -23,7 +23,8 @@ from orchestrator.utils.search_query import create_sqlalchemy_select
|
|
|
23
23
|
logger = structlog.get_logger(__name__)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
@make_async
|
|
27
|
+
def resolve_resource_types(
|
|
27
28
|
info: OrchestratorInfo,
|
|
28
29
|
filter_by: list[GraphqlFilter] | None = None,
|
|
29
30
|
sort_by: list[GraphqlSort] | None = None,
|
|
@@ -3,6 +3,7 @@ import structlog
|
|
|
3
3
|
from orchestrator.db.filters import Filter
|
|
4
4
|
from orchestrator.db.sorting import Sort
|
|
5
5
|
from orchestrator.graphql.pagination import Connection
|
|
6
|
+
from orchestrator.graphql.resolvers.helpers import make_async
|
|
6
7
|
from orchestrator.graphql.schemas.scheduled_task import ScheduledTaskGraphql
|
|
7
8
|
from orchestrator.graphql.types import GraphqlFilter, GraphqlSort, OrchestratorInfo
|
|
8
9
|
from orchestrator.graphql.utils import create_resolver_error_handler, to_graphql_result_page
|
|
@@ -12,7 +13,8 @@ from orchestrator.schedules.scheduler import get_scheduler_tasks, scheduled_task
|
|
|
12
13
|
logger = structlog.get_logger(__name__)
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
@make_async
|
|
17
|
+
def resolve_scheduled_tasks(
|
|
16
18
|
info: OrchestratorInfo,
|
|
17
19
|
filter_by: list[GraphqlFilter] | None = None,
|
|
18
20
|
sort_by: list[GraphqlSort] | None = None,
|
|
@@ -4,6 +4,7 @@ from redis.asyncio import Redis as AIORedis
|
|
|
4
4
|
|
|
5
5
|
from oauth2_lib.strawberry import authenticated_mutation_field
|
|
6
6
|
from orchestrator.api.api_v1.endpoints.settings import generate_engine_status_response
|
|
7
|
+
from orchestrator.graphql.resolvers.helpers import make_async
|
|
7
8
|
from orchestrator.graphql.schemas.errors import Error
|
|
8
9
|
from orchestrator.graphql.schemas.settings import (
|
|
9
10
|
CACHE_FLUSH_OPTIONS,
|
|
@@ -27,6 +28,7 @@ logger = structlog.get_logger(__name__)
|
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
# Queries
|
|
31
|
+
@make_async
|
|
30
32
|
def resolve_settings(info: OrchestratorInfo) -> StatusType:
|
|
31
33
|
selected_fields = get_selected_fields(info)
|
|
32
34
|
|
|
@@ -18,6 +18,7 @@ from graphql import GraphQLError
|
|
|
18
18
|
from pydantic.alias_generators import to_camel as to_lower_camel
|
|
19
19
|
from sqlalchemy import Select, func, select
|
|
20
20
|
from sqlalchemy.orm import contains_eager
|
|
21
|
+
from starlette.concurrency import run_in_threadpool
|
|
21
22
|
from strawberry.experimental.pydantic.conversion_types import StrawberryTypeFromPydantic
|
|
22
23
|
|
|
23
24
|
from nwastdlib.asyncio import gather_nice
|
|
@@ -101,7 +102,7 @@ async def format_subscription(info: OrchestratorInfo, subscription: Subscription
|
|
|
101
102
|
async def resolve_subscription(info: OrchestratorInfo, id: UUID) -> SubscriptionInterface | None:
|
|
102
103
|
stmt = select(SubscriptionTable).where(SubscriptionTable.subscription_id == id)
|
|
103
104
|
|
|
104
|
-
if subscription := db.session.scalar
|
|
105
|
+
if subscription := await run_in_threadpool(db.session.scalar, stmt):
|
|
105
106
|
return await format_subscription(info, subscription)
|
|
106
107
|
return None
|
|
107
108
|
|
|
@@ -141,12 +142,13 @@ async def resolve_subscriptions(
|
|
|
141
142
|
stmt = filter_by_query_string(stmt, query)
|
|
142
143
|
|
|
143
144
|
stmt = cast(Select, sort_subscriptions(stmt, pydantic_sort_by, _error_handler))
|
|
144
|
-
total = db.session.scalar
|
|
145
|
+
total = await run_in_threadpool(db.session.scalar, select(func.count()).select_from(stmt.subquery()))
|
|
145
146
|
stmt = apply_range_to_statement(stmt, after, after + first + 1)
|
|
146
147
|
|
|
147
148
|
graphql_subscriptions: list[SubscriptionInterface] = []
|
|
148
149
|
if is_querying_page_data(info):
|
|
149
|
-
|
|
150
|
+
scalars = await run_in_threadpool(db.session.scalars, stmt)
|
|
151
|
+
subscriptions = scalars.all()
|
|
150
152
|
graphql_subscriptions = list(await gather_nice((format_subscription(info, p) for p in subscriptions))) # type: ignore
|
|
151
153
|
logger.info("Resolve subscriptions", filter_by=filter_by, total=total)
|
|
152
154
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from structlog import get_logger
|
|
2
2
|
|
|
3
3
|
from orchestrator import __version__
|
|
4
|
+
from orchestrator.graphql.resolvers.helpers import make_async
|
|
4
5
|
from orchestrator.graphql.schemas.version import VersionType
|
|
5
6
|
from orchestrator.graphql.types import OrchestratorInfo
|
|
6
7
|
from orchestrator.graphql.utils import create_resolver_error_handler
|
|
@@ -11,6 +12,7 @@ logger = get_logger(__name__)
|
|
|
11
12
|
VERSIONS = [f"orchestrator-core: {__version__}"]
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
@make_async
|
|
14
16
|
def resolve_version(info: OrchestratorInfo) -> VersionType | None:
|
|
15
17
|
logger.debug("resolve_version() called")
|
|
16
18
|
_error_handler = create_resolver_error_handler(info)
|
|
@@ -9,7 +9,7 @@ from orchestrator.db.range.range import apply_range_to_statement
|
|
|
9
9
|
from orchestrator.db.sorting import Sort
|
|
10
10
|
from orchestrator.db.sorting.workflow import sort_workflows, workflow_sort_fields
|
|
11
11
|
from orchestrator.graphql.pagination import Connection
|
|
12
|
-
from orchestrator.graphql.resolvers.helpers import rows_from_statement
|
|
12
|
+
from orchestrator.graphql.resolvers.helpers import make_async, rows_from_statement
|
|
13
13
|
from orchestrator.graphql.schemas.workflow import Workflow
|
|
14
14
|
from orchestrator.graphql.types import GraphqlFilter, GraphqlSort, OrchestratorInfo
|
|
15
15
|
from orchestrator.graphql.utils import create_resolver_error_handler, is_querying_page_data, to_graphql_result_page
|
|
@@ -19,7 +19,8 @@ from orchestrator.utils.search_query import create_sqlalchemy_select
|
|
|
19
19
|
logger = structlog.get_logger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
@make_async
|
|
23
|
+
def resolve_workflows(
|
|
23
24
|
info: OrchestratorInfo,
|
|
24
25
|
filter_by: list[GraphqlFilter] | None = None,
|
|
25
26
|
sort_by: list[GraphqlSort] | None = None,
|