orchestrator-core 4.7.1__py3-none-any.whl → 4.7.2rc1__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 +17 -2
- orchestrator/db/models.py +42 -12
- orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to_.py +32 -0
- orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to__downgrade.sql +138 -0
- orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to__upgrade.sql +153 -0
- orchestrator/schemas/product.py +4 -2
- orchestrator/schemas/product_block.py +4 -2
- orchestrator/schemas/resource_type.py +4 -2
- orchestrator/schemas/search.py +7 -0
- orchestrator/schemas/workflow.py +3 -2
- orchestrator/search/query/engine.py +41 -7
- orchestrator/search/query/results.py +22 -2
- orchestrator/services/processes.py +9 -4
- orchestrator/workflows/modify_note.py +6 -1
- {orchestrator_core-4.7.1.dist-info → orchestrator_core-4.7.2rc1.dist-info}/METADATA +4 -4
- {orchestrator_core-4.7.1.dist-info → orchestrator_core-4.7.2rc1.dist-info}/RECORD +19 -16
- {orchestrator_core-4.7.1.dist-info → orchestrator_core-4.7.2rc1.dist-info}/WHEEL +0 -0
- {orchestrator_core-4.7.1.dist-info → orchestrator_core-4.7.2rc1.dist-info}/licenses/LICENSE +0 -0
orchestrator/__init__.py
CHANGED
|
@@ -16,6 +16,7 @@ from fastapi import APIRouter, HTTPException, Query, status
|
|
|
16
16
|
|
|
17
17
|
from orchestrator.db import db
|
|
18
18
|
from orchestrator.schemas.search import (
|
|
19
|
+
CursorInfoSchema,
|
|
19
20
|
ExportResponse,
|
|
20
21
|
PageInfoSchema,
|
|
21
22
|
PathsResponse,
|
|
@@ -81,10 +82,24 @@ async def _perform_search_and_fetch(
|
|
|
81
82
|
|
|
82
83
|
next_page_cursor = encode_next_page_cursor(search_response, page_cursor, query)
|
|
83
84
|
has_next_page = next_page_cursor is not None
|
|
84
|
-
page_info = PageInfoSchema(
|
|
85
|
+
page_info = PageInfoSchema(
|
|
86
|
+
has_next_page=has_next_page,
|
|
87
|
+
next_page_cursor=next_page_cursor,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
cursor_info = None
|
|
91
|
+
if search_response.total_items:
|
|
92
|
+
cursor_info = CursorInfoSchema(
|
|
93
|
+
total_items=search_response.total_items,
|
|
94
|
+
start_cursor=search_response.start_cursor,
|
|
95
|
+
end_cursor=search_response.end_cursor,
|
|
96
|
+
)
|
|
85
97
|
|
|
86
98
|
return SearchResultsSchema(
|
|
87
|
-
data=search_response.results,
|
|
99
|
+
data=search_response.results,
|
|
100
|
+
page_info=page_info,
|
|
101
|
+
search_metadata=search_response.metadata,
|
|
102
|
+
cursor=cursor_info,
|
|
88
103
|
)
|
|
89
104
|
except (InvalidCursorError, ValueError) as e:
|
|
90
105
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
|
orchestrator/db/models.py
CHANGED
|
@@ -38,7 +38,6 @@ from sqlalchemy import (
|
|
|
38
38
|
Select,
|
|
39
39
|
String,
|
|
40
40
|
Table,
|
|
41
|
-
Text,
|
|
42
41
|
TypeDecorator,
|
|
43
42
|
UniqueConstraint,
|
|
44
43
|
select,
|
|
@@ -69,6 +68,37 @@ logger = structlog.get_logger(__name__)
|
|
|
69
68
|
TAG_LENGTH = 20
|
|
70
69
|
STATUS_LENGTH = 255
|
|
71
70
|
|
|
71
|
+
# Field length limits chosen based on expected usage patterns
|
|
72
|
+
# These values are intended to be reasonable, but give lots of wiggle room
|
|
73
|
+
# If these values are updated, they also need to be updated in a migration, as in migration d69e10434a04
|
|
74
|
+
NOTE_LENGTH = 5000
|
|
75
|
+
DESCRIPTION_LENGTH = 2000
|
|
76
|
+
FAILED_REASON_LENGTH = 10000
|
|
77
|
+
TRACEBACK_LENGTH = 50000
|
|
78
|
+
RESOURCE_VALUE_LENGTH = 10000
|
|
79
|
+
DOMAIN_MODEL_ATTR_LENGTH = 255
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class StringThatAutoConvertsToNullWhenEmpty(TypeDecorator):
|
|
83
|
+
"""A String type that converts empty strings to NULL on save."""
|
|
84
|
+
|
|
85
|
+
impl = String
|
|
86
|
+
cache_ok = True
|
|
87
|
+
python_type = str
|
|
88
|
+
|
|
89
|
+
def __init__(self, length: int | None = None):
|
|
90
|
+
super().__init__(length)
|
|
91
|
+
|
|
92
|
+
def process_bind_param(self, value: str | None, dialect: Dialect) -> str | None:
|
|
93
|
+
"""Called when saving to DB - convert empty/whitespace to NULL."""
|
|
94
|
+
if value is not None and value.strip() == "":
|
|
95
|
+
return None
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
def process_result_value(self, value: str | None, dialect: Dialect) -> str | None:
|
|
99
|
+
"""Called when loading from DB - return as-is."""
|
|
100
|
+
return value
|
|
101
|
+
|
|
72
102
|
|
|
73
103
|
class UtcTimestampError(Exception, DontWrapMixin):
|
|
74
104
|
pass
|
|
@@ -122,8 +152,8 @@ class ProcessTable(BaseModel):
|
|
|
122
152
|
last_modified_at = mapped_column(
|
|
123
153
|
UtcTimestamp, server_default=text("current_timestamp()"), onupdate=nowtz, nullable=False
|
|
124
154
|
)
|
|
125
|
-
failed_reason = mapped_column(
|
|
126
|
-
traceback = mapped_column(
|
|
155
|
+
failed_reason = mapped_column(String(FAILED_REASON_LENGTH))
|
|
156
|
+
traceback = mapped_column(String(TRACEBACK_LENGTH))
|
|
127
157
|
created_by = mapped_column(String(255), nullable=True)
|
|
128
158
|
is_task = mapped_column(Boolean, nullable=False, server_default=text("false"), index=True)
|
|
129
159
|
|
|
@@ -223,7 +253,7 @@ class ProductTable(BaseModel):
|
|
|
223
253
|
|
|
224
254
|
product_id = mapped_column(UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True)
|
|
225
255
|
name = mapped_column(String(), nullable=False, unique=True)
|
|
226
|
-
description = mapped_column(
|
|
256
|
+
description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
|
|
227
257
|
product_type = mapped_column(String(255), nullable=False)
|
|
228
258
|
tag = mapped_column(String(TAG_LENGTH), nullable=False, index=True)
|
|
229
259
|
status = mapped_column(String(STATUS_LENGTH), nullable=False)
|
|
@@ -302,7 +332,7 @@ class ProductBlockTable(BaseModel):
|
|
|
302
332
|
|
|
303
333
|
product_block_id = mapped_column(UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True)
|
|
304
334
|
name = mapped_column(String(), nullable=False, unique=True)
|
|
305
|
-
description = mapped_column(
|
|
335
|
+
description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
|
|
306
336
|
tag = mapped_column(String(TAG_LENGTH))
|
|
307
337
|
status = mapped_column(String(STATUS_LENGTH))
|
|
308
338
|
created_at = mapped_column(UtcTimestamp, nullable=False, server_default=text("current_timestamp()"))
|
|
@@ -401,7 +431,7 @@ class ResourceTypeTable(BaseModel):
|
|
|
401
431
|
|
|
402
432
|
resource_type_id = mapped_column(UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True)
|
|
403
433
|
resource_type = mapped_column(String(510), nullable=False, unique=True)
|
|
404
|
-
description = mapped_column(
|
|
434
|
+
description = mapped_column(String(DESCRIPTION_LENGTH))
|
|
405
435
|
|
|
406
436
|
product_blocks = relationship(
|
|
407
437
|
"ProductBlockTable", secondary=product_block_resource_type_association, back_populates="resource_types"
|
|
@@ -414,7 +444,7 @@ class WorkflowTable(BaseModel):
|
|
|
414
444
|
workflow_id = mapped_column(UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True)
|
|
415
445
|
name = mapped_column(String(), nullable=False, unique=True)
|
|
416
446
|
target = mapped_column(String(), nullable=False)
|
|
417
|
-
description = mapped_column(
|
|
447
|
+
description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
|
|
418
448
|
created_at = mapped_column(UtcTimestamp, nullable=False, server_default=text("current_timestamp()"))
|
|
419
449
|
deleted_at = mapped_column(UtcTimestamp, deferred=True)
|
|
420
450
|
|
|
@@ -452,7 +482,7 @@ class SubscriptionInstanceRelationTable(BaseModel):
|
|
|
452
482
|
|
|
453
483
|
# Needed to make sure subscription instance is populated in the right domain model attribute, if more than one
|
|
454
484
|
# attribute uses the same product block model.
|
|
455
|
-
domain_model_attr = Column(
|
|
485
|
+
domain_model_attr = Column(String(DOMAIN_MODEL_ATTR_LENGTH))
|
|
456
486
|
|
|
457
487
|
in_use_by: Mapped[SubscriptionInstanceTable] = relationship(
|
|
458
488
|
"SubscriptionInstanceTable", back_populates="depends_on_block_relations", foreign_keys=[in_use_by_id]
|
|
@@ -559,7 +589,7 @@ class SubscriptionInstanceValueTable(BaseModel):
|
|
|
559
589
|
resource_type_id = mapped_column(
|
|
560
590
|
UUIDType, ForeignKey("resource_types.resource_type_id"), nullable=False, index=True
|
|
561
591
|
)
|
|
562
|
-
value = mapped_column(
|
|
592
|
+
value = mapped_column(String(RESOURCE_VALUE_LENGTH), nullable=False)
|
|
563
593
|
|
|
564
594
|
resource_type = relationship("ResourceTypeTable", lazy="subquery")
|
|
565
595
|
subscription_instance = relationship("SubscriptionInstanceTable", back_populates="values")
|
|
@@ -587,7 +617,7 @@ class SubscriptionCustomerDescriptionTable(BaseModel):
|
|
|
587
617
|
index=True,
|
|
588
618
|
)
|
|
589
619
|
customer_id = mapped_column(String, nullable=False, index=True)
|
|
590
|
-
description = mapped_column(
|
|
620
|
+
description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
|
|
591
621
|
created_at = mapped_column(UtcTimestamp, nullable=False, server_default=text("current_timestamp()"))
|
|
592
622
|
version = mapped_column(Integer, nullable=False, server_default="1")
|
|
593
623
|
|
|
@@ -600,14 +630,14 @@ class SubscriptionTable(BaseModel):
|
|
|
600
630
|
subscription_id = mapped_column(
|
|
601
631
|
UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True, nullable=False
|
|
602
632
|
)
|
|
603
|
-
description = mapped_column(
|
|
633
|
+
description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
|
|
604
634
|
status = mapped_column(String(STATUS_LENGTH), nullable=False, index=True)
|
|
605
635
|
product_id = mapped_column(UUIDType, ForeignKey("products.product_id"), nullable=False, index=True)
|
|
606
636
|
customer_id = mapped_column(String, index=True, nullable=False)
|
|
607
637
|
insync = mapped_column(Boolean(), nullable=False)
|
|
608
638
|
start_date = mapped_column(UtcTimestamp, nullable=True)
|
|
609
639
|
end_date = mapped_column(UtcTimestamp)
|
|
610
|
-
note = mapped_column(
|
|
640
|
+
note = mapped_column(StringThatAutoConvertsToNullWhenEmpty(NOTE_LENGTH))
|
|
611
641
|
version = mapped_column(Integer, nullable=False, server_default="1")
|
|
612
642
|
|
|
613
643
|
product = relationship("ProductTable", foreign_keys=[product_id])
|
orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to_.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Convert unlimited text fields to limited nullable strings and normalize empty subscription notes.
|
|
2
|
+
|
|
3
|
+
Revision ID: d69e10434a04
|
|
4
|
+
Revises: 9736496e3eba
|
|
5
|
+
Create Date: 2026-01-12 14:17:58.255515
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision = "d69e10434a04"
|
|
16
|
+
down_revision = "9736496e3eba"
|
|
17
|
+
branch_labels = None
|
|
18
|
+
depends_on = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
conn = op.get_bind()
|
|
23
|
+
revision_file_path = Path(__file__.replace(".py", "_upgrade.sql"))
|
|
24
|
+
with open(revision_file_path) as f:
|
|
25
|
+
conn.execute(sa.text(f.read()))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def downgrade() -> None:
|
|
29
|
+
conn = op.get_bind()
|
|
30
|
+
revision_file_path = Path(__file__.replace(".py", "_downgrade.sql"))
|
|
31
|
+
with open(revision_file_path) as f:
|
|
32
|
+
conn.execute(sa.text(f.read()))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
-- Downgrade: Revert limited nullable strings back to unlimited text fields.
|
|
2
|
+
-- Revision ID: d69e10434a04
|
|
3
|
+
|
|
4
|
+
-- Drop triggers first
|
|
5
|
+
DROP TRIGGER IF EXISTS fi_refresh_search ON fixed_inputs;
|
|
6
|
+
DROP TRIGGER IF EXISTS products_refresh_search ON products;
|
|
7
|
+
DROP TRIGGER IF EXISTS sub_cust_desc_refresh_search ON subscription_customer_descriptions;
|
|
8
|
+
DROP TRIGGER IF EXISTS siv_refresh_search ON subscription_instance_values;
|
|
9
|
+
DROP TRIGGER IF EXISTS sub_refresh_search ON subscriptions;
|
|
10
|
+
|
|
11
|
+
-- Drop the refresh function
|
|
12
|
+
DROP FUNCTION IF EXISTS refresh_subscriptions_search_view();
|
|
13
|
+
|
|
14
|
+
-- Drop the materialized view (CASCADE will drop indexes)
|
|
15
|
+
DROP MATERIALIZED VIEW IF EXISTS subscriptions_search CASCADE;
|
|
16
|
+
|
|
17
|
+
-- Alter column types back from VARCHAR to TEXT
|
|
18
|
+
ALTER TABLE subscriptions ALTER COLUMN note TYPE TEXT;
|
|
19
|
+
ALTER TABLE subscriptions ALTER COLUMN description TYPE TEXT;
|
|
20
|
+
ALTER TABLE processes ALTER COLUMN failed_reason TYPE TEXT;
|
|
21
|
+
ALTER TABLE processes ALTER COLUMN traceback TYPE TEXT;
|
|
22
|
+
ALTER TABLE products ALTER COLUMN description TYPE TEXT;
|
|
23
|
+
ALTER TABLE product_blocks ALTER COLUMN description TYPE TEXT;
|
|
24
|
+
ALTER TABLE resource_types ALTER COLUMN description TYPE TEXT;
|
|
25
|
+
ALTER TABLE workflows ALTER COLUMN description TYPE TEXT;
|
|
26
|
+
ALTER TABLE subscription_customer_descriptions ALTER COLUMN description TYPE TEXT;
|
|
27
|
+
ALTER TABLE subscription_instance_values ALTER COLUMN value TYPE TEXT;
|
|
28
|
+
ALTER TABLE subscription_instance_relations ALTER COLUMN domain_model_attr TYPE TEXT;
|
|
29
|
+
|
|
30
|
+
-- Recreate the materialized view
|
|
31
|
+
CREATE MATERIALIZED VIEW subscriptions_search AS
|
|
32
|
+
WITH rt_info AS (SELECT s.subscription_id,
|
|
33
|
+
concat_ws(
|
|
34
|
+
' ',
|
|
35
|
+
string_agg(rt.resource_type || ' ' || siv.value, ' '),
|
|
36
|
+
string_agg(distinct 'subscription_instance_id' || ':' || si.subscription_instance_id, ' ')
|
|
37
|
+
) AS rt_info
|
|
38
|
+
FROM subscription_instance_values siv
|
|
39
|
+
JOIN resource_types rt ON siv.resource_type_id = rt.resource_type_id
|
|
40
|
+
JOIN subscription_instances si ON siv.subscription_instance_id = si.subscription_instance_id
|
|
41
|
+
JOIN subscriptions s ON si.subscription_id = s.subscription_id
|
|
42
|
+
GROUP BY s.subscription_id),
|
|
43
|
+
sub_prod_info AS (SELECT s.subscription_id,
|
|
44
|
+
array_to_string(
|
|
45
|
+
ARRAY ['subscription_id:' || s.subscription_id,
|
|
46
|
+
'status:' || s.status,
|
|
47
|
+
'insync:' || s.insync,
|
|
48
|
+
'subscription_description:' || s.description,
|
|
49
|
+
'note:' || coalesce(s.note, ''),
|
|
50
|
+
'customer_id:' || s.customer_id,
|
|
51
|
+
'product_id:' || s.product_id],
|
|
52
|
+
' '
|
|
53
|
+
) AS sub_info,
|
|
54
|
+
array_to_string(
|
|
55
|
+
ARRAY ['product_name:' || p.name,
|
|
56
|
+
'product_description:' || p.description,
|
|
57
|
+
'tag:' || p.tag,
|
|
58
|
+
'product_type:', p.product_type],
|
|
59
|
+
' '
|
|
60
|
+
) AS prod_info
|
|
61
|
+
FROM subscriptions s
|
|
62
|
+
JOIN products p ON s.product_id = p.product_id),
|
|
63
|
+
fi_info AS (SELECT s.subscription_id,
|
|
64
|
+
string_agg(fi.name || ':' || fi.value, ' ') AS fi_info
|
|
65
|
+
FROM subscriptions s
|
|
66
|
+
JOIN products p ON s.product_id = p.product_id
|
|
67
|
+
JOIN fixed_inputs fi ON p.product_id = fi.product_id
|
|
68
|
+
GROUP BY s.subscription_id),
|
|
69
|
+
cust_info AS (SELECT s.subscription_id,
|
|
70
|
+
string_agg('customer_description: ' || scd.description, ' ') AS cust_info
|
|
71
|
+
FROM subscriptions s
|
|
72
|
+
JOIN subscription_customer_descriptions scd ON s.subscription_id = scd.subscription_id
|
|
73
|
+
GROUP BY s.subscription_id)
|
|
74
|
+
-- to_tsvector handles parsing of hyphened words in a peculiar way and is inconsistent with how to_tsquery parses it in Postgres <14
|
|
75
|
+
-- Replacing all hyphens with underscores makes the parsing more predictable and removes some issues arising when searching for subscription ids for example
|
|
76
|
+
-- See: https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=0c4f355c6a5fd437f71349f2f3d5d491382572b7
|
|
77
|
+
SELECT s.subscription_id,
|
|
78
|
+
to_tsvector(
|
|
79
|
+
'simple',
|
|
80
|
+
replace(
|
|
81
|
+
concat_ws(
|
|
82
|
+
' ',
|
|
83
|
+
spi.sub_info,
|
|
84
|
+
spi.prod_info,
|
|
85
|
+
fi.fi_info,
|
|
86
|
+
rti.rt_info,
|
|
87
|
+
ci.cust_info,
|
|
88
|
+
md.metadata::text
|
|
89
|
+
),
|
|
90
|
+
'-', '_')
|
|
91
|
+
) as tsv
|
|
92
|
+
FROM subscriptions s
|
|
93
|
+
LEFT JOIN sub_prod_info spi ON s.subscription_id = spi.subscription_id
|
|
94
|
+
LEFT JOIN fi_info fi ON s.subscription_id = fi.subscription_id
|
|
95
|
+
LEFT JOIN rt_info rti ON s.subscription_id = rti.subscription_id
|
|
96
|
+
LEFT JOIN cust_info ci ON s.subscription_id = ci.subscription_id
|
|
97
|
+
LEFT JOIN subscription_metadata md ON s.subscription_id = md.subscription_id;
|
|
98
|
+
|
|
99
|
+
-- Create indexes
|
|
100
|
+
CREATE INDEX subscriptions_search_tsv_idx ON subscriptions_search USING GIN (tsv);
|
|
101
|
+
CREATE UNIQUE INDEX subscriptions_search_subscription_id_idx ON subscriptions_search (subscription_id);
|
|
102
|
+
|
|
103
|
+
-- Create refresh function with epoch-based throttling
|
|
104
|
+
CREATE OR REPLACE FUNCTION refresh_subscriptions_search_view()
|
|
105
|
+
RETURNS TRIGGER
|
|
106
|
+
LANGUAGE plpgsql
|
|
107
|
+
AS
|
|
108
|
+
$$
|
|
109
|
+
DECLARE
|
|
110
|
+
should_refresh bool;
|
|
111
|
+
current_epoch int;
|
|
112
|
+
last_refresh_epoch int;
|
|
113
|
+
comment_sql text;
|
|
114
|
+
BEGIN
|
|
115
|
+
SELECT extract(epoch from now())::int INTO current_epoch;
|
|
116
|
+
SELECT coalesce(pg_catalog.obj_description('subscriptions_search'::regclass)::int, 0) INTO last_refresh_epoch;
|
|
117
|
+
|
|
118
|
+
SELECT (current_epoch - last_refresh_epoch) > 120 INTO should_refresh;
|
|
119
|
+
|
|
120
|
+
IF should_refresh THEN
|
|
121
|
+
REFRESH MATERIALIZED VIEW CONCURRENTLY subscriptions_search;
|
|
122
|
+
|
|
123
|
+
comment_sql := 'COMMENT ON MATERIALIZED VIEW subscriptions_search IS ' || quote_literal(current_epoch);
|
|
124
|
+
EXECUTE comment_sql;
|
|
125
|
+
END IF;
|
|
126
|
+
RETURN NULL;
|
|
127
|
+
END;
|
|
128
|
+
$$;
|
|
129
|
+
|
|
130
|
+
-- Create triggers
|
|
131
|
+
CREATE CONSTRAINT TRIGGER fi_refresh_search AFTER UPDATE ON fixed_inputs DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
132
|
+
CREATE CONSTRAINT TRIGGER products_refresh_search AFTER UPDATE ON products DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
133
|
+
CREATE CONSTRAINT TRIGGER sub_cust_desc_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscription_customer_descriptions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
134
|
+
CREATE CONSTRAINT TRIGGER siv_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscription_instance_values DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
135
|
+
CREATE CONSTRAINT TRIGGER sub_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscriptions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
136
|
+
|
|
137
|
+
-- Refresh the materialized view
|
|
138
|
+
REFRESH MATERIALIZED VIEW subscriptions_search;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
-- Convert unlimited text fields to limited nullable strings and normalize empty subscription notes.
|
|
2
|
+
-- Revision ID: d69e10434a04
|
|
3
|
+
|
|
4
|
+
-- Field length limits chosen based on expected usage patterns
|
|
5
|
+
-- These values are intended to be reasonable, but give lots of wiggle room
|
|
6
|
+
-- If these values are updated, they also need to be updated in orchestrator-core/orchestrator/db/models.py
|
|
7
|
+
-- NOTE_LENGTH = 5000
|
|
8
|
+
-- DESCRIPTION_LENGTH = 2000
|
|
9
|
+
-- FAILED_REASON_LENGTH = 10000
|
|
10
|
+
-- TRACEBACK_LENGTH = 50000
|
|
11
|
+
-- RESOURCE_VALUE_LENGTH = 10000
|
|
12
|
+
-- DOMAIN_MODEL_ATTR_LENGTH = 255
|
|
13
|
+
|
|
14
|
+
-- Drop triggers first
|
|
15
|
+
DROP TRIGGER IF EXISTS fi_refresh_search ON fixed_inputs;
|
|
16
|
+
DROP TRIGGER IF EXISTS products_refresh_search ON products;
|
|
17
|
+
DROP TRIGGER IF EXISTS sub_cust_desc_refresh_search ON subscription_customer_descriptions;
|
|
18
|
+
DROP TRIGGER IF EXISTS siv_refresh_search ON subscription_instance_values;
|
|
19
|
+
DROP TRIGGER IF EXISTS sub_refresh_search ON subscriptions;
|
|
20
|
+
|
|
21
|
+
-- Drop the refresh function
|
|
22
|
+
DROP FUNCTION IF EXISTS refresh_subscriptions_search_view();
|
|
23
|
+
|
|
24
|
+
-- Drop the materialized view (CASCADE will drop indexes)
|
|
25
|
+
DROP MATERIALIZED VIEW IF EXISTS subscriptions_search CASCADE;
|
|
26
|
+
|
|
27
|
+
-- Normalize empty strings to NULL before altering column types
|
|
28
|
+
UPDATE subscriptions
|
|
29
|
+
SET note = NULL
|
|
30
|
+
WHERE note IS NOT NULL AND TRIM(note) = '';
|
|
31
|
+
|
|
32
|
+
-- Alter column types from TEXT to VARCHAR with limits
|
|
33
|
+
ALTER TABLE subscriptions ALTER COLUMN note TYPE VARCHAR(5000);
|
|
34
|
+
ALTER TABLE subscriptions ALTER COLUMN description TYPE VARCHAR(2000);
|
|
35
|
+
ALTER TABLE processes ALTER COLUMN failed_reason TYPE VARCHAR(10000);
|
|
36
|
+
ALTER TABLE processes ALTER COLUMN traceback TYPE VARCHAR(50000);
|
|
37
|
+
ALTER TABLE products ALTER COLUMN description TYPE VARCHAR(2000);
|
|
38
|
+
ALTER TABLE product_blocks ALTER COLUMN description TYPE VARCHAR(2000);
|
|
39
|
+
ALTER TABLE resource_types ALTER COLUMN description TYPE VARCHAR(2000);
|
|
40
|
+
ALTER TABLE workflows ALTER COLUMN description TYPE VARCHAR(2000);
|
|
41
|
+
ALTER TABLE subscription_customer_descriptions ALTER COLUMN description TYPE VARCHAR(2000);
|
|
42
|
+
ALTER TABLE subscription_instance_values ALTER COLUMN value TYPE VARCHAR(10000);
|
|
43
|
+
ALTER TABLE subscription_instance_relations ALTER COLUMN domain_model_attr TYPE VARCHAR(255);
|
|
44
|
+
|
|
45
|
+
-- Recreate the materialized view
|
|
46
|
+
CREATE MATERIALIZED VIEW subscriptions_search AS
|
|
47
|
+
WITH rt_info AS (SELECT s.subscription_id,
|
|
48
|
+
concat_ws(
|
|
49
|
+
' ',
|
|
50
|
+
string_agg(rt.resource_type || ' ' || siv.value, ' '),
|
|
51
|
+
string_agg(distinct 'subscription_instance_id' || ':' || si.subscription_instance_id, ' ')
|
|
52
|
+
) AS rt_info
|
|
53
|
+
FROM subscription_instance_values siv
|
|
54
|
+
JOIN resource_types rt ON siv.resource_type_id = rt.resource_type_id
|
|
55
|
+
JOIN subscription_instances si ON siv.subscription_instance_id = si.subscription_instance_id
|
|
56
|
+
JOIN subscriptions s ON si.subscription_id = s.subscription_id
|
|
57
|
+
GROUP BY s.subscription_id),
|
|
58
|
+
sub_prod_info AS (SELECT s.subscription_id,
|
|
59
|
+
array_to_string(
|
|
60
|
+
ARRAY ['subscription_id:' || s.subscription_id,
|
|
61
|
+
'status:' || s.status,
|
|
62
|
+
'insync:' || s.insync,
|
|
63
|
+
'subscription_description:' || s.description,
|
|
64
|
+
'note:' || coalesce(s.note, ''),
|
|
65
|
+
'customer_id:' || s.customer_id,
|
|
66
|
+
'product_id:' || s.product_id],
|
|
67
|
+
' '
|
|
68
|
+
) AS sub_info,
|
|
69
|
+
array_to_string(
|
|
70
|
+
ARRAY ['product_name:' || p.name,
|
|
71
|
+
'product_description:' || p.description,
|
|
72
|
+
'tag:' || p.tag,
|
|
73
|
+
'product_type:', p.product_type],
|
|
74
|
+
' '
|
|
75
|
+
) AS prod_info
|
|
76
|
+
FROM subscriptions s
|
|
77
|
+
JOIN products p ON s.product_id = p.product_id),
|
|
78
|
+
fi_info AS (SELECT s.subscription_id,
|
|
79
|
+
string_agg(fi.name || ':' || fi.value, ' ') AS fi_info
|
|
80
|
+
FROM subscriptions s
|
|
81
|
+
JOIN products p ON s.product_id = p.product_id
|
|
82
|
+
JOIN fixed_inputs fi ON p.product_id = fi.product_id
|
|
83
|
+
GROUP BY s.subscription_id),
|
|
84
|
+
cust_info AS (SELECT s.subscription_id,
|
|
85
|
+
string_agg('customer_description: ' || scd.description, ' ') AS cust_info
|
|
86
|
+
FROM subscriptions s
|
|
87
|
+
JOIN subscription_customer_descriptions scd ON s.subscription_id = scd.subscription_id
|
|
88
|
+
GROUP BY s.subscription_id)
|
|
89
|
+
-- to_tsvector handles parsing of hyphened words in a peculiar way and is inconsistent with how to_tsquery parses it in Postgres <14
|
|
90
|
+
-- Replacing all hyphens with underscores makes the parsing more predictable and removes some issues arising when searching for subscription ids for example
|
|
91
|
+
-- See: https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=0c4f355c6a5fd437f71349f2f3d5d491382572b7
|
|
92
|
+
SELECT s.subscription_id,
|
|
93
|
+
to_tsvector(
|
|
94
|
+
'simple',
|
|
95
|
+
replace(
|
|
96
|
+
concat_ws(
|
|
97
|
+
' ',
|
|
98
|
+
spi.sub_info,
|
|
99
|
+
spi.prod_info,
|
|
100
|
+
fi.fi_info,
|
|
101
|
+
rti.rt_info,
|
|
102
|
+
ci.cust_info,
|
|
103
|
+
md.metadata::text
|
|
104
|
+
),
|
|
105
|
+
'-', '_')
|
|
106
|
+
) as tsv
|
|
107
|
+
FROM subscriptions s
|
|
108
|
+
LEFT JOIN sub_prod_info spi ON s.subscription_id = spi.subscription_id
|
|
109
|
+
LEFT JOIN fi_info fi ON s.subscription_id = fi.subscription_id
|
|
110
|
+
LEFT JOIN rt_info rti ON s.subscription_id = rti.subscription_id
|
|
111
|
+
LEFT JOIN cust_info ci ON s.subscription_id = ci.subscription_id
|
|
112
|
+
LEFT JOIN subscription_metadata md ON s.subscription_id = md.subscription_id;
|
|
113
|
+
|
|
114
|
+
-- Create indexes
|
|
115
|
+
CREATE INDEX subscriptions_search_tsv_idx ON subscriptions_search USING GIN (tsv);
|
|
116
|
+
CREATE UNIQUE INDEX subscriptions_search_subscription_id_idx ON subscriptions_search (subscription_id);
|
|
117
|
+
|
|
118
|
+
-- Create refresh function with epoch-based throttling
|
|
119
|
+
CREATE OR REPLACE FUNCTION refresh_subscriptions_search_view()
|
|
120
|
+
RETURNS TRIGGER
|
|
121
|
+
LANGUAGE plpgsql
|
|
122
|
+
AS
|
|
123
|
+
$$
|
|
124
|
+
DECLARE
|
|
125
|
+
should_refresh bool;
|
|
126
|
+
current_epoch int;
|
|
127
|
+
last_refresh_epoch int;
|
|
128
|
+
comment_sql text;
|
|
129
|
+
BEGIN
|
|
130
|
+
SELECT extract(epoch from now())::int INTO current_epoch;
|
|
131
|
+
SELECT coalesce(pg_catalog.obj_description('subscriptions_search'::regclass)::int, 0) INTO last_refresh_epoch;
|
|
132
|
+
|
|
133
|
+
SELECT (current_epoch - last_refresh_epoch) > 120 INTO should_refresh;
|
|
134
|
+
|
|
135
|
+
IF should_refresh THEN
|
|
136
|
+
REFRESH MATERIALIZED VIEW CONCURRENTLY subscriptions_search;
|
|
137
|
+
|
|
138
|
+
comment_sql := 'COMMENT ON MATERIALIZED VIEW subscriptions_search IS ' || quote_literal(current_epoch);
|
|
139
|
+
EXECUTE comment_sql;
|
|
140
|
+
END IF;
|
|
141
|
+
RETURN NULL;
|
|
142
|
+
END;
|
|
143
|
+
$$;
|
|
144
|
+
|
|
145
|
+
-- Create triggers
|
|
146
|
+
CREATE CONSTRAINT TRIGGER fi_refresh_search AFTER UPDATE ON fixed_inputs DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
147
|
+
CREATE CONSTRAINT TRIGGER products_refresh_search AFTER UPDATE ON products DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
148
|
+
CREATE CONSTRAINT TRIGGER sub_cust_desc_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscription_customer_descriptions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
149
|
+
CREATE CONSTRAINT TRIGGER siv_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscription_instance_values DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
150
|
+
CREATE CONSTRAINT TRIGGER sub_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscriptions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
|
|
151
|
+
|
|
152
|
+
-- Refresh the materialized view
|
|
153
|
+
REFRESH MATERIALIZED VIEW subscriptions_search;
|
orchestrator/schemas/product.py
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from datetime import datetime
|
|
15
|
+
from typing import Annotated
|
|
15
16
|
from uuid import UUID
|
|
16
17
|
|
|
17
|
-
from pydantic import ConfigDict
|
|
18
|
+
from pydantic import ConfigDict, Field
|
|
18
19
|
|
|
20
|
+
from orchestrator.db.models import DESCRIPTION_LENGTH
|
|
19
21
|
from orchestrator.domain.lifecycle import ProductLifecycle
|
|
20
22
|
from orchestrator.schemas.base import OrchestratorBaseModel
|
|
21
23
|
from orchestrator.schemas.fixed_input import FixedInputSchema
|
|
@@ -44,4 +46,4 @@ class ProductSchema(ProductBaseSchema):
|
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
class ProductPatchSchema(OrchestratorBaseModel):
|
|
47
|
-
description: str | None = None
|
|
49
|
+
description: Annotated[str, Field(max_length=DESCRIPTION_LENGTH)] | None = None
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from datetime import datetime
|
|
15
|
+
from typing import Annotated
|
|
15
16
|
from uuid import UUID
|
|
16
17
|
|
|
17
|
-
from pydantic import ConfigDict
|
|
18
|
+
from pydantic import ConfigDict, Field
|
|
18
19
|
|
|
20
|
+
from orchestrator.db.models import DESCRIPTION_LENGTH
|
|
19
21
|
from orchestrator.domain.lifecycle import ProductLifecycle
|
|
20
22
|
from orchestrator.schemas.base import OrchestratorBaseModel
|
|
21
23
|
from orchestrator.schemas.resource_type import ResourceTypeBaseSchema, ResourceTypeSchema
|
|
@@ -40,4 +42,4 @@ class ProductBlockSchema(ProductBlockBaseSchema):
|
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
class ProductBlockPatchSchema(OrchestratorBaseModel):
|
|
43
|
-
description: str | None = None
|
|
45
|
+
description: Annotated[str, Field(max_length=DESCRIPTION_LENGTH)] | None = None
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
|
+
from typing import Annotated
|
|
14
15
|
from uuid import UUID
|
|
15
16
|
|
|
16
|
-
from pydantic import ConfigDict
|
|
17
|
+
from pydantic import ConfigDict, Field
|
|
17
18
|
|
|
19
|
+
from orchestrator.db.models import DESCRIPTION_LENGTH
|
|
18
20
|
from orchestrator.schemas.base import OrchestratorBaseModel
|
|
19
21
|
|
|
20
22
|
|
|
@@ -30,4 +32,4 @@ class ResourceTypeSchema(ResourceTypeBaseSchema):
|
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class ResourceTypePatchSchema(OrchestratorBaseModel):
|
|
33
|
-
description: str | None = None
|
|
35
|
+
description: Annotated[str, Field(max_length=DESCRIPTION_LENGTH)] | None = None
|
orchestrator/schemas/search.py
CHANGED
|
@@ -34,10 +34,17 @@ class ProductSchema(BaseModel):
|
|
|
34
34
|
product_type: str
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
class CursorInfoSchema(BaseModel):
|
|
38
|
+
total_items: int | None = None
|
|
39
|
+
start_cursor: int | None = None
|
|
40
|
+
end_cursor: int | None = None
|
|
41
|
+
|
|
42
|
+
|
|
37
43
|
class SearchResultsSchema(BaseModel, Generic[T]):
|
|
38
44
|
data: list[T] = Field(default_factory=list)
|
|
39
45
|
page_info: PageInfoSchema = Field(default_factory=PageInfoSchema)
|
|
40
46
|
search_metadata: SearchMetadata | None = None
|
|
47
|
+
cursor: CursorInfoSchema | None = None
|
|
41
48
|
|
|
42
49
|
|
|
43
50
|
class PathsResponse(BaseModel):
|
orchestrator/schemas/workflow.py
CHANGED
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
from datetime import datetime
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import Annotated, Any
|
|
16
16
|
from uuid import UUID
|
|
17
17
|
|
|
18
18
|
from pydantic import ConfigDict, Field
|
|
19
19
|
|
|
20
|
+
from orchestrator.db.models import DESCRIPTION_LENGTH
|
|
20
21
|
from orchestrator.schemas.base import OrchestratorBaseModel
|
|
21
22
|
from orchestrator.targets import Target
|
|
22
23
|
|
|
@@ -65,4 +66,4 @@ class SubscriptionWorkflowListsSchema(OrchestratorBaseModel):
|
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
class WorkflowPatchSchema(OrchestratorBaseModel):
|
|
68
|
-
description: str | None = None
|
|
69
|
+
description: Annotated[str, Field(max_length=DESCRIPTION_LENGTH)] | None = None
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
|
|
14
14
|
import structlog
|
|
15
|
+
from sqlalchemy import Select, func, select
|
|
15
16
|
from sqlalchemy.orm import Session
|
|
16
17
|
|
|
17
18
|
from orchestrator.search.core.embedding import QueryEmbedder
|
|
@@ -30,6 +31,7 @@ from orchestrator.search.retrieval.retrievers import (
|
|
|
30
31
|
RrfHybridRetriever,
|
|
31
32
|
SemanticRetriever,
|
|
32
33
|
)
|
|
34
|
+
from orchestrator.search.retrieval.retrievers.structured import StructuredRetriever
|
|
33
35
|
|
|
34
36
|
from .builder import build_aggregation_query, build_candidate_query, build_simple_count_query
|
|
35
37
|
from .export import fetch_export_data
|
|
@@ -91,6 +93,28 @@ def _get_retriever_from_override(
|
|
|
91
93
|
)
|
|
92
94
|
|
|
93
95
|
|
|
96
|
+
def _create_cursor_info(
|
|
97
|
+
final_stmt: Select,
|
|
98
|
+
db_session: Session,
|
|
99
|
+
cursor: PageCursor | None,
|
|
100
|
+
query: SelectQuery | ExportQuery,
|
|
101
|
+
query_embedding: list[float] | None,
|
|
102
|
+
candidate_query: Select,
|
|
103
|
+
row_count: int,
|
|
104
|
+
) -> tuple[int | None, int | None, int | None]:
|
|
105
|
+
total_count_stmt = Retriever.route(query, None, query_embedding).apply(candidate_query) if cursor else final_stmt
|
|
106
|
+
total_items_or_none = db_session.scalar(select(func.count()).select_from(total_count_stmt.subquery()))
|
|
107
|
+
total_items = total_items_or_none or 0
|
|
108
|
+
count_with_cursor_or_none = (
|
|
109
|
+
db_session.scalar(select(func.count()).select_from(final_stmt.subquery())) if cursor else total_items
|
|
110
|
+
)
|
|
111
|
+
count_with_cursor = count_with_cursor_or_none or 0
|
|
112
|
+
start_cursor = total_items - count_with_cursor
|
|
113
|
+
row_end_cursor_count = row_count - (2 if row_count > query.limit else 1)
|
|
114
|
+
end_cursor = start_cursor + row_end_cursor_count
|
|
115
|
+
return total_items, start_cursor, end_cursor
|
|
116
|
+
|
|
117
|
+
|
|
94
118
|
async def _execute_search(
|
|
95
119
|
query: SelectQuery | ExportQuery,
|
|
96
120
|
db_session: Session,
|
|
@@ -126,14 +150,24 @@ async def _execute_search(
|
|
|
126
150
|
logger.debug("Using retriever", retriever_type=retriever.__class__.__name__)
|
|
127
151
|
|
|
128
152
|
final_stmt = retriever.apply(candidate_query)
|
|
129
|
-
|
|
130
|
-
logger.debug(
|
|
131
|
-
|
|
153
|
+
final_stmt_with_limit = final_stmt.limit(limit)
|
|
154
|
+
logger.debug(final_stmt_with_limit)
|
|
155
|
+
|
|
156
|
+
result = db_session.execute(final_stmt_with_limit).mappings().all()
|
|
157
|
+
result_rows = list(result)
|
|
158
|
+
row_count = len(result_rows)
|
|
159
|
+
|
|
160
|
+
total_items: int | None = None
|
|
161
|
+
start_cursor: int | None = None
|
|
162
|
+
end_cursor: int | None = None
|
|
163
|
+
if isinstance(retriever, StructuredRetriever) and row_count > 0:
|
|
164
|
+
total_items, start_cursor, end_cursor = _create_cursor_info(
|
|
165
|
+
final_stmt, db_session, cursor, query, query_embedding, candidate_query, row_count
|
|
166
|
+
)
|
|
132
167
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return response
|
|
168
|
+
return format_search_response(
|
|
169
|
+
result_rows, query, retriever.metadata, query_embedding, total_items, start_cursor, end_cursor
|
|
170
|
+
)
|
|
137
171
|
|
|
138
172
|
|
|
139
173
|
async def execute_search(
|
|
@@ -66,6 +66,9 @@ class SearchResponse(BaseModel):
|
|
|
66
66
|
metadata: SearchMetadata
|
|
67
67
|
query_embedding: list[float] | None = None
|
|
68
68
|
has_more: bool = False
|
|
69
|
+
total_items: int | None = None
|
|
70
|
+
start_cursor: int | None = None
|
|
71
|
+
end_cursor: int | None = None
|
|
69
72
|
|
|
70
73
|
|
|
71
74
|
class AggregationResult(BaseModel):
|
|
@@ -221,7 +224,13 @@ def generate_highlight_indices(text: str, term: str) -> list[tuple[int, int]]:
|
|
|
221
224
|
|
|
222
225
|
|
|
223
226
|
def format_search_response(
|
|
224
|
-
db_rows: Sequence[RowMapping],
|
|
227
|
+
db_rows: Sequence[RowMapping],
|
|
228
|
+
query: "SelectQuery | ExportQuery",
|
|
229
|
+
metadata: SearchMetadata,
|
|
230
|
+
query_embedding: list[float] | None,
|
|
231
|
+
total_items: int | None,
|
|
232
|
+
start_cursor: int | None,
|
|
233
|
+
end_cursor: int | None,
|
|
225
234
|
) -> SearchResponse:
|
|
226
235
|
"""Format database query results into a `SearchResponse`.
|
|
227
236
|
|
|
@@ -232,6 +241,10 @@ def format_search_response(
|
|
|
232
241
|
db_rows: The rows returned from the executed SQLAlchemy query.
|
|
233
242
|
query: SelectQuery or ExportQuery with search criteria.
|
|
234
243
|
metadata: Metadata about the search execution.
|
|
244
|
+
query_embedding: query embedding for agent to save to DB.
|
|
245
|
+
total_items: total items of the query (only with structured search).
|
|
246
|
+
start_cursor: start cursor of the db_rows (only with structured search).
|
|
247
|
+
end_cursor: end cursor of the db_rows (only with structured search).
|
|
235
248
|
|
|
236
249
|
Returns:
|
|
237
250
|
SearchResponse: A list of `SearchResult` objects containing entity IDs, scores,
|
|
@@ -280,7 +293,14 @@ def format_search_response(
|
|
|
280
293
|
matching_field=matching_field,
|
|
281
294
|
)
|
|
282
295
|
)
|
|
283
|
-
return SearchResponse(
|
|
296
|
+
return SearchResponse(
|
|
297
|
+
results=results,
|
|
298
|
+
metadata=metadata,
|
|
299
|
+
total_items=total_items,
|
|
300
|
+
start_cursor=start_cursor,
|
|
301
|
+
end_cursor=end_cursor,
|
|
302
|
+
query_embedding=query_embedding,
|
|
303
|
+
)
|
|
284
304
|
|
|
285
305
|
|
|
286
306
|
def _extract_matching_field_from_filters(filters: "FilterTree") -> MatchingField | None:
|
|
@@ -30,6 +30,7 @@ from oauth2_lib.fastapi import OIDCUserModel
|
|
|
30
30
|
from orchestrator.api.error_handling import raise_status
|
|
31
31
|
from orchestrator.config.assignee import Assignee
|
|
32
32
|
from orchestrator.db import EngineSettingsTable, ProcessStepTable, ProcessSubscriptionTable, ProcessTable, db
|
|
33
|
+
from orchestrator.db.models import FAILED_REASON_LENGTH, TRACEBACK_LENGTH
|
|
33
34
|
from orchestrator.distlock import distlock_manager
|
|
34
35
|
from orchestrator.schemas.engine_settings import WorkerStatus
|
|
35
36
|
from orchestrator.services.input_state import store_input_state
|
|
@@ -139,8 +140,9 @@ def _update_process(process_id: UUID, step: Step, process_state: WFProcess) -> P
|
|
|
139
140
|
# pop also removes the traceback from the dict
|
|
140
141
|
traceback = step_state.pop("traceback", None)
|
|
141
142
|
|
|
142
|
-
|
|
143
|
-
p.
|
|
143
|
+
# Truncate failed_reason (from end) and traceback (from start) to fit database constraints
|
|
144
|
+
p.failed_reason = failed_reason[:FAILED_REASON_LENGTH] if failed_reason else failed_reason
|
|
145
|
+
p.traceback = traceback[-TRACEBACK_LENGTH:] if traceback else traceback
|
|
144
146
|
|
|
145
147
|
if process_state.isfailed() and p.is_task:
|
|
146
148
|
# Check if we need a special failed status:
|
|
@@ -341,8 +343,11 @@ def _db_log_process_ex(process_id: UUID, ex: Exception) -> None:
|
|
|
341
343
|
p.last_step = "Unknown"
|
|
342
344
|
if p.last_status != ProcessStatus.WAITING:
|
|
343
345
|
p.last_status = ProcessStatus.FAILED
|
|
344
|
-
|
|
345
|
-
|
|
346
|
+
failed_reason = str(ex)
|
|
347
|
+
traceback = show_ex(ex)
|
|
348
|
+
# Truncate failed_reason (from end) and traceback (from start) to fit database constraints
|
|
349
|
+
p.failed_reason = failed_reason[:FAILED_REASON_LENGTH] if failed_reason else failed_reason
|
|
350
|
+
p.traceback = traceback[-TRACEBACK_LENGTH:] if traceback else traceback
|
|
346
351
|
db.session.add(p)
|
|
347
352
|
try:
|
|
348
353
|
db.session.commit()
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
|
|
13
17
|
from orchestrator.db import db
|
|
18
|
+
from orchestrator.db.models import NOTE_LENGTH
|
|
14
19
|
from orchestrator.forms import SubmitFormPage
|
|
15
20
|
from orchestrator.services import subscriptions
|
|
16
21
|
from orchestrator.settings import get_authorizers
|
|
@@ -31,7 +36,7 @@ def initial_input_form(subscription_id: UUIDstr) -> FormGenerator:
|
|
|
31
36
|
old_note = subscription.note
|
|
32
37
|
|
|
33
38
|
class ModifyNoteForm(SubmitFormPage):
|
|
34
|
-
note: LongText = old_note
|
|
39
|
+
note: Annotated[LongText, Field(max_length=NOTE_LENGTH)] = old_note
|
|
35
40
|
|
|
36
41
|
user_input = yield ModifyNoteForm
|
|
37
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: orchestrator-core
|
|
3
|
-
Version: 4.7.
|
|
3
|
+
Version: 4.7.2rc1
|
|
4
4
|
Summary: This is the orchestrator workflow engine.
|
|
5
5
|
Author-email: SURF <automation-beheer@surf.nl>
|
|
6
6
|
Requires-Python: >=3.11,<3.15
|
|
@@ -31,7 +31,7 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
31
31
|
Classifier: Topic :: Software Development
|
|
32
32
|
Classifier: Typing :: Typed
|
|
33
33
|
License-File: LICENSE
|
|
34
|
-
Requires-Dist: alembic==1.18.
|
|
34
|
+
Requires-Dist: alembic==1.18.3
|
|
35
35
|
Requires-Dist: anyio>=3.7.0
|
|
36
36
|
Requires-Dist: apscheduler>=3.11.0
|
|
37
37
|
Requires-Dist: click==8.*
|
|
@@ -44,7 +44,7 @@ Requires-Dist: jinja2==3.1.6
|
|
|
44
44
|
Requires-Dist: more-itertools~=10.8.0
|
|
45
45
|
Requires-Dist: nwa-stdlib~=1.11.0
|
|
46
46
|
Requires-Dist: oauth2-lib>=2.5.0
|
|
47
|
-
Requires-Dist: orjson==3.11.
|
|
47
|
+
Requires-Dist: orjson==3.11.7
|
|
48
48
|
Requires-Dist: pgvector>=0.4.1
|
|
49
49
|
Requires-Dist: prometheus-client==0.24.1
|
|
50
50
|
Requires-Dist: psycopg2-binary==2.9.11
|
|
@@ -57,7 +57,7 @@ Requires-Dist: pytz==2025.2
|
|
|
57
57
|
Requires-Dist: redis==7.1.0
|
|
58
58
|
Requires-Dist: semver==3.0.4
|
|
59
59
|
Requires-Dist: sentry-sdk[fastapi]>=2.29.1
|
|
60
|
-
Requires-Dist: sqlalchemy==2.0.
|
|
60
|
+
Requires-Dist: sqlalchemy==2.0.46
|
|
61
61
|
Requires-Dist: sqlalchemy-utils==0.42.1
|
|
62
62
|
Requires-Dist: strawberry-graphql>=0.281.0,<0.285.0
|
|
63
63
|
Requires-Dist: structlog>=25.4.0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
orchestrator/__init__.py,sha256=
|
|
1
|
+
orchestrator/__init__.py,sha256=p4XGF-AzKy7JAUS3i9s8xJYE20rWVg8KZxv3Cu3VCYY,1457
|
|
2
2
|
orchestrator/agentic_app.py,sha256=ouiyyZiS4uS6Lox2DtbGGRnb2njJBMSHpSAGe-T5rX0,3028
|
|
3
3
|
orchestrator/app.py,sha256=5ITGSN_KeRi2qTvfwBXhjOGNyWNy-rdtzfOLEk76ZtY,14661
|
|
4
4
|
orchestrator/exception_handlers.py,sha256=UsW3dw8q0QQlNLcV359bIotah8DYjMsj2Ts1LfX4ClY,1268
|
|
@@ -25,7 +25,7 @@ orchestrator/api/api_v1/endpoints/product_blocks.py,sha256=kZ6ywIOsS_S2qGq7RvZ4K
|
|
|
25
25
|
orchestrator/api/api_v1/endpoints/products.py,sha256=BfFtwu9dZXEQbtKxYj9icc73GKGvAGMR5ytyf41nQlQ,3081
|
|
26
26
|
orchestrator/api/api_v1/endpoints/resource_types.py,sha256=gGyuaDyOD0TAVoeFGaGmjDGnQ8eQQArOxKrrk4MaDzA,2145
|
|
27
27
|
orchestrator/api/api_v1/endpoints/schedules.py,sha256=eTG_4CQkiIi2akJUN4xDGuU_OvF6Ml6uye5MmQ_WJbc,1731
|
|
28
|
-
orchestrator/api/api_v1/endpoints/search.py,sha256=
|
|
28
|
+
orchestrator/api/api_v1/endpoints/search.py,sha256=363lacZRRexlfLbL4dVRhgT1ir1u43dEKYt4zHuWxH0,8986
|
|
29
29
|
orchestrator/api/api_v1/endpoints/settings.py,sha256=5s-k169podZjgGHUbVDmSQwpY_3Cs_Bbf2PPtZIkBcw,6184
|
|
30
30
|
orchestrator/api/api_v1/endpoints/subscription_customer_descriptions.py,sha256=1_6LtgQleoq3M6z_W-Qz__Bj3OFUweoPrUqHMwSH6AM,3288
|
|
31
31
|
orchestrator/api/api_v1/endpoints/subscriptions.py,sha256=7KaodccUiMkcVnrFnK2azp_V_-hGudcIyhov5WwVGQY,9810
|
|
@@ -117,7 +117,7 @@ orchestrator/db/database.py,sha256=MU_w_e95ho2dVb2JDnt_KFYholx___XDkiQXbc8wCkI,1
|
|
|
117
117
|
orchestrator/db/helpers.py,sha256=L8kEdnSSNGnUpZhdeGx2arCodakWN8vSpKdfjoLuHdY,831
|
|
118
118
|
orchestrator/db/listeners.py,sha256=UBPYcH0FE3a7aZQu_D0O_JMXpXIRYXC0gjSAvlv5GZo,1142
|
|
119
119
|
orchestrator/db/loaders.py,sha256=ez6JzQ3IKVkC_oLAkVlIIiI8Do7hXbdcPKCvUSLxRog,7962
|
|
120
|
-
orchestrator/db/models.py,sha256=
|
|
120
|
+
orchestrator/db/models.py,sha256=oc0MAwWzQn3KpKWMEHR_oxmQ09NW1g6vzU4Pqv_Brk0,33551
|
|
121
121
|
orchestrator/db/filters/__init__.py,sha256=RUj6P0XxEBhYj0SN5wH5-Vf_Wt_ilZR_n9DSar5m9oM,371
|
|
122
122
|
orchestrator/db/filters/filters.py,sha256=55RtpQwM2rhrk4A6CCSeSXoo-BT9GnQoNTryA8CtLEg,5020
|
|
123
123
|
orchestrator/db/filters/process.py,sha256=xvGhyfo_MZ1xhLvFC6yULjcT4mJk0fKc1glJIYgsWLE,4018
|
|
@@ -260,6 +260,9 @@ orchestrator/migrations/versions/schema/2025-07-04_4b58e336d1bf_deprecating_work
|
|
|
260
260
|
orchestrator/migrations/versions/schema/2025-07-28_850dccac3b02_update_description_of_resume_workflows_.py,sha256=R6Qoga83DJ1IL0WYPu0u5u2ZvAmqGlDmUMv_KtJyOhQ,812
|
|
261
261
|
orchestrator/migrations/versions/schema/2025-11-18_961eddbd4c13_create_linker_table_workflow_apscheduler.py,sha256=Vy2qA8wb_lQWExhF0PX_IFwCr_vafe9uaT1pXvCwbGI,3227
|
|
262
262
|
orchestrator/migrations/versions/schema/2025-12-10_9736496e3eba_set_is_task_true_on_certain_tasks.py,sha256=2DOERJ7QF83o-goxJPtz0FUC3xZAt5ms8miadFGVFcw,1007
|
|
263
|
+
orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to_.py,sha256=kIOBK9ft3UA5BTbNROYKD5usXBJT7FqgNMcQLEZOqAw,813
|
|
264
|
+
orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to__downgrade.sql,sha256=xTEZEwEXIUVlNyR7HeMKrXKgcYryOI_CGRqPbCGXRYo,7683
|
|
265
|
+
orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to__upgrade.sql,sha256=RvUmpkZohnxsMXmBVBz0oPALEYkEEMYxRP-_MrrNbEk,8387
|
|
263
266
|
orchestrator/schedules/__init__.py,sha256=i8sT88A3v_5KIfwbKZxe3rS2rMakOuqfAis0DRmBleU,1017
|
|
264
267
|
orchestrator/schedules/scheduler.py,sha256=8o7DoVs9Q1Q231FVMpv3tXtKbaydeNkYQ1h6kl7U1X4,7198
|
|
265
268
|
orchestrator/schedules/scheduling.py,sha256=1lSeAhKRGhZNOtFiB-FPMeo3bEIDpt9OdJKBkk7QknI,2914
|
|
@@ -271,15 +274,15 @@ orchestrator/schemas/engine_settings.py,sha256=LF8al7tJssiilb5A4emPtUYo0tVDSaT1L
|
|
|
271
274
|
orchestrator/schemas/fixed_input.py,sha256=Rth3hT5K7zYuQr1bUY_NJRzb03xEZuT1p6EvYXVNE54,1214
|
|
272
275
|
orchestrator/schemas/problem_detail.py,sha256=DxiUhWv6EVXLZgdKFv0EYVnCgtkDj7xteDCR0q2f5yw,802
|
|
273
276
|
orchestrator/schemas/process.py,sha256=UACBNt-4g4v9Y528u-gZ-Wk7YxwJHhnI4cEu5CtQm2w,2541
|
|
274
|
-
orchestrator/schemas/product.py,sha256=
|
|
275
|
-
orchestrator/schemas/product_block.py,sha256=
|
|
276
|
-
orchestrator/schemas/resource_type.py,sha256=
|
|
277
|
+
orchestrator/schemas/product.py,sha256=oovz_HAY2asih_sFiwzMyRmdSmJMymFIRPSPx3L-66I,1709
|
|
278
|
+
orchestrator/schemas/product_block.py,sha256=pIRQUxI6Rm_T6o4MiY6bzrLVKYZSWCR-vUQsjMa52Bg,1658
|
|
279
|
+
orchestrator/schemas/resource_type.py,sha256=AhYKUe8NVWgQx4DOCCgQ4CFcJA0pAX9EE5OCLQYq0z4,1203
|
|
277
280
|
orchestrator/schemas/schedules.py,sha256=Gb427IGR5mPTjKN8STwUhAWCJMCywJkrS8OetiiHTKY,2844
|
|
278
|
-
orchestrator/schemas/search.py,sha256=
|
|
281
|
+
orchestrator/schemas/search.py,sha256=UJ4R8zZ4rnXybOCi7KPDzA6TYaCTVqmHeDxrKJxny5U,1731
|
|
279
282
|
orchestrator/schemas/search_requests.py,sha256=2gb1mbzzMmSbMTtLmItrTSPWRXNKwdgoPIEiNFhTFjA,2144
|
|
280
283
|
orchestrator/schemas/subscription.py,sha256=-jXyHZIed9Xlia18ksSDyenblNN6Q2yM2FlGELyJ458,3423
|
|
281
284
|
orchestrator/schemas/subscription_descriptions.py,sha256=Ft_jw1U0bf9Z0U8O4OWfLlcl0mXCVT_qYVagBP3GbIQ,1262
|
|
282
|
-
orchestrator/schemas/workflow.py,sha256=
|
|
285
|
+
orchestrator/schemas/workflow.py,sha256=d0zJIf6XM7ny1-y-7uDYF2pJFbWPCRXn-PC04PUH9i0,2260
|
|
283
286
|
orchestrator/search/__init__.py,sha256=2uhTQexKx-cdBP1retV3CYSNCs02s8WL3fhGvupRGZk,571
|
|
284
287
|
orchestrator/search/llm_migration.py,sha256=afDqdo5t-L6gLduKSSVKuAegznPdIsQn4YIoAAFraC8,6767
|
|
285
288
|
orchestrator/search/agent/__init__.py,sha256=_O4DN0MSTUtr4olhyE0-2hsb7x3f_KURMCYjg8jV4QA,756
|
|
@@ -312,12 +315,12 @@ orchestrator/search/indexing/tasks.py,sha256=0p68RNwJnHSGZQjfdpyFsS2Ma5Gr2PpZROZ
|
|
|
312
315
|
orchestrator/search/indexing/traverse.py,sha256=JLut9t4LoPCWhJ_63VgYhRKfjwyxRv-mTbQLC8mA_mU,15158
|
|
313
316
|
orchestrator/search/query/__init__.py,sha256=nCjvK_n2WQdV_ACrncFXEfnvLcHtuI__J7KLlFIaQvo,2437
|
|
314
317
|
orchestrator/search/query/builder.py,sha256=EfDSSOQKUBNtUESDBsKaPY6hZ_iDXAwc3qcNR4AGAEg,13261
|
|
315
|
-
orchestrator/search/query/engine.py,sha256=
|
|
318
|
+
orchestrator/search/query/engine.py,sha256=M6UFHjU5ovLiPPvhXGgM7xCCCZZyD6Hgh13niPgjj6U,9427
|
|
316
319
|
orchestrator/search/query/exceptions.py,sha256=DrkNzXVbQAOi28FTHKimf_eTrXmhYwXrH986QhfQLPU,4941
|
|
317
320
|
orchestrator/search/query/export.py,sha256=_0ncVpTqN6AoQfW3WX0fWnDQX3hBz6ZGC31Beu4PVwQ,6678
|
|
318
321
|
orchestrator/search/query/mixins.py,sha256=8zvrQMlIkWt3q0BFfekm9ugVmuu85GaKQBEgJxUQmj4,5178
|
|
319
322
|
orchestrator/search/query/queries.py,sha256=0jF97cU2Z98-oWm1Iyqf3xIgrmc7FcWAPTb51tUG4MA,4506
|
|
320
|
-
orchestrator/search/query/results.py,sha256=
|
|
323
|
+
orchestrator/search/query/results.py,sha256=CtCF9iJV0_Z0PSqiD8XhfVruqVKaBFNuRNmpjKVZoKQ,11695
|
|
321
324
|
orchestrator/search/query/state.py,sha256=fMSBJs39kZTkpDE2T4h4x0x-51GqUvzAuePg2YUbO6I,3220
|
|
322
325
|
orchestrator/search/query/validation.py,sha256=Pprv40yvpynL1-MCFE1YuouguYW6lfh1PZKsVei2i6w,9622
|
|
323
326
|
orchestrator/search/retrieval/__init__.py,sha256=q5G0z3nKjIHKFs1PkEG3nvTUy3Wp4kCyBtCbqUITj3A,579
|
|
@@ -333,7 +336,7 @@ orchestrator/services/__init__.py,sha256=GyHNfEFCGKQwRiN6rQmvSRH2iYX7npjMZn97n8X
|
|
|
333
336
|
orchestrator/services/fixed_inputs.py,sha256=kyz7s2HLzyDulvcq-ZqefTw1om86COvyvTjz0_5CmgI,876
|
|
334
337
|
orchestrator/services/input_state.py,sha256=6BZOpb3cHpO18K-XG-3QUIV9pIM25_ufdODrp5CmXG4,2390
|
|
335
338
|
orchestrator/services/process_broadcast_thread.py,sha256=D44YbjF8mRqGuznkRUV4SoRn1J0lfy_x1H508GnSVlU,4649
|
|
336
|
-
orchestrator/services/processes.py,sha256=
|
|
339
|
+
orchestrator/services/processes.py,sha256=FGCzlj-B0PYeYIbjwIwCEPNTKZUnoOAeEQuXq-dQ5Ds,30892
|
|
337
340
|
orchestrator/services/products.py,sha256=BP4KyE8zO-8z7Trrs5T6zKBOw53S9BfBJnHWI3p6u5Y,1943
|
|
338
341
|
orchestrator/services/resource_types.py,sha256=_QBy_JOW_X3aSTqH0CuLrq4zBJL0p7Q-UDJUcuK2_qc,884
|
|
339
342
|
orchestrator/services/settings.py,sha256=HEWfFulgoEDwgfxGEO__QTr5fDiwNBEj1UhAeTAdbLQ,3159
|
|
@@ -372,7 +375,7 @@ orchestrator/websocket/websocket_manager.py,sha256=hwlG9FDXcNU42jDNNsPMQLIyrvEpG
|
|
|
372
375
|
orchestrator/websocket/managers/broadcast_websocket_manager.py,sha256=fwoSgTjkHJ2GmsLTU9dqQpAA9i8b1McPu7gLNzxtfG4,5401
|
|
373
376
|
orchestrator/websocket/managers/memory_websocket_manager.py,sha256=lF5EEx1iFMCGEkTbItTDr88NENMSaSeG1QrJ7teoPkY,3324
|
|
374
377
|
orchestrator/workflows/__init__.py,sha256=FbwcAYJh8oSi0QFjXXXomdl9c8whCa_qSt_vPXcwasE,4216
|
|
375
|
-
orchestrator/workflows/modify_note.py,sha256=
|
|
378
|
+
orchestrator/workflows/modify_note.py,sha256=dSNXsthIVqv0BwbYO33ZORCpbDLoNFNqnSr3NNBKMmI,2507
|
|
376
379
|
orchestrator/workflows/removed_workflow.py,sha256=fwi1-aC1KQvb08hq8St-_lWOLM_tjTcQMLJ_Fjdn2M8,1111
|
|
377
380
|
orchestrator/workflows/steps.py,sha256=VVLRK9_7KzrBlnK7L8eSmRMNVOO7VJBh5OSjHQHM9fU,7019
|
|
378
381
|
orchestrator/workflows/utils.py,sha256=VUCDoIl5XAKtIeAJpVpyW2pCIg3PoVWfwGn28BYlYhA,15424
|
|
@@ -383,7 +386,7 @@ orchestrator/workflows/tasks/validate_product_type.py,sha256=KoDMqROGVQ0ZPu69jMF
|
|
|
383
386
|
orchestrator/workflows/tasks/validate_products.py,sha256=lCAXmCVhohgrdgJn7-d7fIxPj4MVOX0J8KezcvwIK3k,8716
|
|
384
387
|
orchestrator/workflows/tasks/validate_subscriptions.py,sha256=CFKf3igyrqGm-zzSMD0wbNLgJwKz8quNNgnNWDqgpI0,2387
|
|
385
388
|
orchestrator/workflows/translations/en-GB.json,sha256=ObBlH9XILJ9uNaGcJexi3IB0e6P8CKFKRgu29luIEM8,973
|
|
386
|
-
orchestrator_core-4.7.
|
|
387
|
-
orchestrator_core-4.7.
|
|
388
|
-
orchestrator_core-4.7.
|
|
389
|
-
orchestrator_core-4.7.
|
|
389
|
+
orchestrator_core-4.7.2rc1.dist-info/licenses/LICENSE,sha256=b-aA5OZQuuBATmLKo_mln8CQrDPPhg3ghLzjPjLn4Tg,11409
|
|
390
|
+
orchestrator_core-4.7.2rc1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
391
|
+
orchestrator_core-4.7.2rc1.dist-info/METADATA,sha256=sNJgIhSwYBLt30MlpBniJDGtuQ-VY9bLIGtvJ2hUYrA,6421
|
|
392
|
+
orchestrator_core-4.7.2rc1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|