orchestrator-core 4.5.2__py3-none-any.whl → 4.6.0__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.
Files changed (53) hide show
  1. orchestrator/__init__.py +2 -2
  2. orchestrator/agentic_app.py +3 -23
  3. orchestrator/api/api_v1/api.py +5 -0
  4. orchestrator/api/api_v1/endpoints/agent.py +49 -0
  5. orchestrator/api/api_v1/endpoints/search.py +120 -201
  6. orchestrator/app.py +1 -1
  7. orchestrator/cli/database.py +3 -0
  8. orchestrator/cli/generate.py +11 -4
  9. orchestrator/cli/generator/generator/migration.py +7 -3
  10. orchestrator/cli/main.py +1 -1
  11. orchestrator/cli/scheduler.py +15 -22
  12. orchestrator/cli/search/resize_embedding.py +28 -22
  13. orchestrator/cli/search/speedtest.py +4 -6
  14. orchestrator/db/__init__.py +6 -0
  15. orchestrator/db/models.py +75 -0
  16. orchestrator/llm_settings.py +18 -1
  17. orchestrator/migrations/helpers.py +47 -39
  18. orchestrator/schedules/scheduler.py +32 -15
  19. orchestrator/schedules/validate_products.py +1 -1
  20. orchestrator/schemas/search.py +8 -85
  21. orchestrator/search/agent/__init__.py +2 -2
  22. orchestrator/search/agent/agent.py +26 -30
  23. orchestrator/search/agent/json_patch.py +51 -0
  24. orchestrator/search/agent/prompts.py +35 -9
  25. orchestrator/search/agent/state.py +28 -2
  26. orchestrator/search/agent/tools.py +192 -53
  27. orchestrator/search/core/embedding.py +2 -2
  28. orchestrator/search/core/exceptions.py +6 -0
  29. orchestrator/search/core/types.py +1 -0
  30. orchestrator/search/export.py +199 -0
  31. orchestrator/search/indexing/indexer.py +13 -4
  32. orchestrator/search/indexing/registry.py +14 -1
  33. orchestrator/search/llm_migration.py +55 -0
  34. orchestrator/search/retrieval/__init__.py +3 -2
  35. orchestrator/search/retrieval/builder.py +5 -1
  36. orchestrator/search/retrieval/engine.py +66 -23
  37. orchestrator/search/retrieval/pagination.py +46 -56
  38. orchestrator/search/retrieval/query_state.py +61 -0
  39. orchestrator/search/retrieval/retrievers/base.py +26 -40
  40. orchestrator/search/retrieval/retrievers/fuzzy.py +10 -9
  41. orchestrator/search/retrieval/retrievers/hybrid.py +11 -8
  42. orchestrator/search/retrieval/retrievers/semantic.py +9 -8
  43. orchestrator/search/retrieval/retrievers/structured.py +6 -6
  44. orchestrator/search/schemas/parameters.py +17 -13
  45. orchestrator/search/schemas/results.py +4 -1
  46. orchestrator/settings.py +1 -0
  47. orchestrator/utils/auth.py +3 -2
  48. orchestrator/workflow.py +23 -6
  49. orchestrator/workflows/tasks/validate_product_type.py +3 -3
  50. {orchestrator_core-4.5.2.dist-info → orchestrator_core-4.6.0.dist-info}/METADATA +17 -12
  51. {orchestrator_core-4.5.2.dist-info → orchestrator_core-4.6.0.dist-info}/RECORD +53 -49
  52. {orchestrator_core-4.5.2.dist-info → orchestrator_core-4.6.0.dist-info}/WHEEL +0 -0
  53. {orchestrator_core-4.5.2.dist-info → orchestrator_core-4.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -20,7 +20,7 @@ from sqlalchemy.types import TypeEngine
20
20
  from orchestrator.db.models import AiSearchIndex
21
21
  from orchestrator.search.core.types import SearchMetadata
22
22
 
23
- from ..pagination import PaginationParams
23
+ from ..pagination import PageCursor
24
24
  from .base import Retriever
25
25
 
26
26
 
@@ -127,14 +127,13 @@ class RrfHybridRetriever(Retriever):
127
127
  self,
128
128
  q_vec: list[float],
129
129
  fuzzy_term: str,
130
- pagination_params: PaginationParams,
130
+ cursor: PageCursor | None,
131
131
  k: int = 60,
132
132
  field_candidates_limit: int = 100,
133
133
  ) -> None:
134
134
  self.q_vec = q_vec
135
135
  self.fuzzy_term = fuzzy_term
136
- self.page_after_score = pagination_params.page_after_score
137
- self.page_after_id = pagination_params.page_after_id
136
+ self.cursor = cursor
138
137
  self.k = k
139
138
  self.field_candidates_limit = field_candidates_limit
140
139
 
@@ -154,6 +153,7 @@ class RrfHybridRetriever(Retriever):
154
153
  field_candidates = (
155
154
  select(
156
155
  AiSearchIndex.entity_id,
156
+ AiSearchIndex.entity_title,
157
157
  AiSearchIndex.path,
158
158
  AiSearchIndex.value,
159
159
  sem_val,
@@ -178,9 +178,10 @@ class RrfHybridRetriever(Retriever):
178
178
  entity_scores = (
179
179
  select(
180
180
  field_candidates.c.entity_id,
181
+ field_candidates.c.entity_title,
181
182
  func.avg(field_candidates.c.semantic_distance).label("avg_semantic_distance"),
182
183
  func.avg(field_candidates.c.fuzzy_score).label("avg_fuzzy_score"),
183
- ).group_by(field_candidates.c.entity_id)
184
+ ).group_by(field_candidates.c.entity_id, field_candidates.c.entity_title)
184
185
  ).cte("entity_scores")
185
186
 
186
187
  entity_highlights = (
@@ -204,6 +205,7 @@ class RrfHybridRetriever(Retriever):
204
205
  ranked = (
205
206
  select(
206
207
  entity_scores.c.entity_id,
208
+ entity_scores.c.entity_title,
207
209
  entity_scores.c.avg_semantic_distance,
208
210
  entity_scores.c.avg_fuzzy_score,
209
211
  entity_highlights.c.highlight_text,
@@ -242,6 +244,7 @@ class RrfHybridRetriever(Retriever):
242
244
 
243
245
  stmt = select(
244
246
  ranked.c.entity_id,
247
+ ranked.c.entity_title,
245
248
  score,
246
249
  ranked.c.highlight_text,
247
250
  ranked.c.highlight_path,
@@ -262,12 +265,12 @@ class RrfHybridRetriever(Retriever):
262
265
  entity_id_column: ColumnElement,
263
266
  ) -> Select:
264
267
  """Keyset paginate by fused score + id."""
265
- if self.page_after_score is not None and self.page_after_id is not None:
266
- score_param = self._quantize_score_for_pagination(self.page_after_score)
268
+ if self.cursor is not None:
269
+ score_param = self._quantize_score_for_pagination(self.cursor.score)
267
270
  stmt = stmt.where(
268
271
  or_(
269
272
  score_column < score_param,
270
- and_(score_column == score_param, entity_id_column > self.page_after_id),
273
+ and_(score_column == score_param, entity_id_column > self.cursor.id),
271
274
  )
272
275
  )
273
276
  return stmt
@@ -17,17 +17,16 @@ from sqlalchemy.sql.expression import ColumnElement
17
17
  from orchestrator.db.models import AiSearchIndex
18
18
  from orchestrator.search.core.types import SearchMetadata
19
19
 
20
- from ..pagination import PaginationParams
20
+ from ..pagination import PageCursor
21
21
  from .base import Retriever
22
22
 
23
23
 
24
24
  class SemanticRetriever(Retriever):
25
25
  """Ranks results based on the minimum semantic vector distance."""
26
26
 
27
- def __init__(self, vector_query: list[float], pagination_params: PaginationParams) -> None:
27
+ def __init__(self, vector_query: list[float], cursor: PageCursor | None) -> None:
28
28
  self.vector_query = vector_query
29
- self.page_after_score = pagination_params.page_after_score
30
- self.page_after_id = pagination_params.page_after_id
29
+ self.cursor = cursor
31
30
 
32
31
  def apply(self, candidate_query: Select) -> Select:
33
32
  cand = candidate_query.subquery()
@@ -49,6 +48,7 @@ class SemanticRetriever(Retriever):
49
48
  combined_query = (
50
49
  select(
51
50
  AiSearchIndex.entity_id,
51
+ AiSearchIndex.entity_title,
52
52
  score,
53
53
  func.first_value(AiSearchIndex.value)
54
54
  .over(partition_by=AiSearchIndex.entity_id, order_by=[dist.asc(), AiSearchIndex.path.asc()])
@@ -60,12 +60,13 @@ class SemanticRetriever(Retriever):
60
60
  .select_from(AiSearchIndex)
61
61
  .join(cand, cand.c.entity_id == AiSearchIndex.entity_id)
62
62
  .where(AiSearchIndex.embedding.isnot(None))
63
- .distinct(AiSearchIndex.entity_id)
63
+ .distinct(AiSearchIndex.entity_id, AiSearchIndex.entity_title)
64
64
  )
65
65
  final_query = combined_query.subquery("ranked_semantic")
66
66
 
67
67
  stmt = select(
68
68
  final_query.c.entity_id,
69
+ final_query.c.entity_title,
69
70
  final_query.c.score,
70
71
  final_query.c.highlight_text,
71
72
  final_query.c.highlight_path,
@@ -83,12 +84,12 @@ class SemanticRetriever(Retriever):
83
84
  self, stmt: Select, score_column: ColumnElement, entity_id_column: ColumnElement
84
85
  ) -> Select:
85
86
  """Apply semantic score pagination with precise Decimal handling."""
86
- if self.page_after_score is not None and self.page_after_id is not None:
87
- score_param = self._quantize_score_for_pagination(self.page_after_score)
87
+ if self.cursor is not None:
88
+ score_param = self._quantize_score_for_pagination(self.cursor.score)
88
89
  stmt = stmt.where(
89
90
  or_(
90
91
  score_column < score_param,
91
- and_(score_column == score_param, entity_id_column > self.page_after_id),
92
+ and_(score_column == score_param, entity_id_column > self.cursor.id),
92
93
  )
93
94
  )
94
95
  return stmt
@@ -15,22 +15,22 @@ from sqlalchemy import Select, literal, select
15
15
 
16
16
  from orchestrator.search.core.types import SearchMetadata
17
17
 
18
- from ..pagination import PaginationParams
18
+ from ..pagination import PageCursor
19
19
  from .base import Retriever
20
20
 
21
21
 
22
22
  class StructuredRetriever(Retriever):
23
23
  """Applies a dummy score for purely structured searches with no text query."""
24
24
 
25
- def __init__(self, pagination_params: PaginationParams) -> None:
26
- self.page_after_id = pagination_params.page_after_id
25
+ def __init__(self, cursor: PageCursor | None) -> None:
26
+ self.cursor = cursor
27
27
 
28
28
  def apply(self, candidate_query: Select) -> Select:
29
29
  cand = candidate_query.subquery()
30
- stmt = select(cand.c.entity_id, literal(1.0).label("score")).select_from(cand)
30
+ stmt = select(cand.c.entity_id, cand.c.entity_title, literal(1.0).label("score")).select_from(cand)
31
31
 
32
- if self.page_after_id:
33
- stmt = stmt.where(cand.c.entity_id > self.page_after_id)
32
+ if self.cursor is not None:
33
+ stmt = stmt.where(cand.c.entity_id > self.cursor.id)
34
34
 
35
35
  return stmt.order_by(cand.c.entity_id.asc())
36
36
 
@@ -12,9 +12,9 @@
12
12
  # limitations under the License.
13
13
 
14
14
  import uuid
15
- from typing import Any, Literal
15
+ from typing import Any, ClassVar, Literal
16
16
 
17
- from pydantic import BaseModel, ConfigDict, Field
17
+ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
18
18
 
19
19
  from orchestrator.search.core.types import ActionType, EntityType
20
20
  from orchestrator.search.filters import FilterTree
@@ -23,6 +23,9 @@ from orchestrator.search.filters import FilterTree
23
23
  class BaseSearchParameters(BaseModel):
24
24
  """Base model with common search parameters."""
25
25
 
26
+ DEFAULT_EXPORT_LIMIT: ClassVar[int] = 1000
27
+ MAX_EXPORT_LIMIT: ClassVar[int] = 10000
28
+
26
29
  action: ActionType = Field(default=ActionType.SELECT, description="The action to perform.")
27
30
  entity_type: EntityType
28
31
 
@@ -33,14 +36,18 @@ class BaseSearchParameters(BaseModel):
33
36
  )
34
37
 
35
38
  limit: int = Field(default=10, ge=1, le=30, description="Maximum number of search results to return.")
39
+ export_limit: int = Field(
40
+ default=DEFAULT_EXPORT_LIMIT, ge=1, le=MAX_EXPORT_LIMIT, description="Maximum number of results to export."
41
+ )
36
42
  model_config = ConfigDict(extra="forbid")
37
43
 
38
44
  @classmethod
39
- def create(cls, entity_type: EntityType, **kwargs: Any) -> "BaseSearchParameters":
40
- try:
41
- return PARAMETER_REGISTRY[entity_type](entity_type=entity_type, **kwargs)
42
- except KeyError:
43
- raise ValueError(f"No search parameter class found for entity type: {entity_type.value}")
45
+ def create(cls, **kwargs: Any) -> "SearchParameters":
46
+ """Create the correct search parameter subclass instance based on entity_type."""
47
+ from orchestrator.search.schemas.parameters import SearchParameters
48
+
49
+ adapter: TypeAdapter = TypeAdapter(SearchParameters)
50
+ return adapter.validate_python(kwargs)
44
51
 
45
52
  @property
46
53
  def vector_query(self) -> str | None:
@@ -121,9 +128,6 @@ class ProcessSearchParameters(BaseSearchParameters):
121
128
  )
122
129
 
123
130
 
124
- PARAMETER_REGISTRY: dict[EntityType, type[BaseSearchParameters]] = {
125
- EntityType.SUBSCRIPTION: SubscriptionSearchParameters,
126
- EntityType.PRODUCT: ProductSearchParameters,
127
- EntityType.WORKFLOW: WorkflowSearchParameters,
128
- EntityType.PROCESS: ProcessSearchParameters,
129
- }
131
+ SearchParameters = (
132
+ SubscriptionSearchParameters | ProductSearchParameters | WorkflowSearchParameters | ProcessSearchParameters
133
+ )
@@ -15,7 +15,7 @@ from typing import Literal
15
15
 
16
16
  from pydantic import BaseModel, ConfigDict
17
17
 
18
- from orchestrator.search.core.types import FilterOp, SearchMetadata, UIType
18
+ from orchestrator.search.core.types import EntityType, FilterOp, SearchMetadata, UIType
19
19
 
20
20
 
21
21
  class MatchingField(BaseModel):
@@ -30,6 +30,8 @@ class SearchResult(BaseModel):
30
30
  """Represents a single search result item."""
31
31
 
32
32
  entity_id: str
33
+ entity_type: EntityType
34
+ entity_title: str
33
35
  score: float
34
36
  perfect_match: int = 0
35
37
  matching_field: MatchingField | None = None
@@ -40,6 +42,7 @@ class SearchResponse(BaseModel):
40
42
 
41
43
  results: list[SearchResult]
42
44
  metadata: SearchMetadata
45
+ query_embedding: list[float] | None = None
43
46
 
44
47
 
45
48
  class ValueSchema(BaseModel):
orchestrator/settings.py CHANGED
@@ -57,6 +57,7 @@ class AppSettings(BaseSettings):
57
57
  EXECUTOR: str = ExecutorType.THREADPOOL
58
58
  WORKFLOWS_SWAGGER_HOST: str = "localhost"
59
59
  WORKFLOWS_GUI_URI: str = "http://localhost:3000"
60
+ BASE_URL: str = "http://localhost:8080" # Base URL for the API (used for generating export URLs)
60
61
  DATABASE_URI: PostgresDsn = "postgresql://nwa:nwa@localhost/orchestrator-core" # type: ignore
61
62
  MAX_WORKERS: int = 5
62
63
  MAIL_SERVER: str = "localhost"
@@ -1,9 +1,10 @@
1
1
  from collections.abc import Callable
2
- from typing import TypeAlias
2
+ from typing import TypeAlias, TypeVar
3
3
 
4
4
  from oauth2_lib.fastapi import OIDCUserModel
5
5
 
6
6
  # This file is broken out separately to avoid circular imports.
7
7
 
8
8
  # Can instead use "type Authorizer = ..." in later Python versions.
9
- Authorizer: TypeAlias = Callable[[OIDCUserModel | None], bool]
9
+ T = TypeVar("T", bound=OIDCUserModel)
10
+ Authorizer: TypeAlias = Callable[[T | None], bool]
orchestrator/workflow.py CHANGED
@@ -235,7 +235,10 @@ def make_workflow(
235
235
  return wrapping_function
236
236
 
237
237
 
238
- def step(name: str) -> Callable[[StepFunc], Step]:
238
+ def step(
239
+ name: str,
240
+ retry_auth_callback: Authorizer | None = None,
241
+ ) -> Callable[[StepFunc], Step]:
239
242
  """Mark a function as a workflow step."""
240
243
 
241
244
  def decorator(func: StepFunc) -> Step:
@@ -255,12 +258,19 @@ def step(name: str) -> Callable[[StepFunc], Step]:
255
258
  logger.warning("Step failed", exc_info=ex)
256
259
  return Failed(ex)
257
260
 
258
- return make_step_function(wrapper, name)
261
+ return make_step_function(
262
+ wrapper,
263
+ name,
264
+ retry_auth_callback=retry_auth_callback,
265
+ )
259
266
 
260
267
  return decorator
261
268
 
262
269
 
263
- def retrystep(name: str) -> Callable[[StepFunc], Step]:
270
+ def retrystep(
271
+ name: str,
272
+ retry_auth_callback: Authorizer | None = None,
273
+ ) -> Callable[[StepFunc], Step]:
264
274
  """Mark a function as a retryable workflow step.
265
275
 
266
276
  If this step fails it goes to `Waiting` were it will be retried periodically. If it `Success` it acts as a normal
@@ -283,7 +293,11 @@ def retrystep(name: str) -> Callable[[StepFunc], Step]:
283
293
  except Exception as ex:
284
294
  return Waiting(ex)
285
295
 
286
- return make_step_function(wrapper, name)
296
+ return make_step_function(
297
+ wrapper,
298
+ name,
299
+ retry_auth_callback=retry_auth_callback,
300
+ )
287
301
 
288
302
  return decorator
289
303
 
@@ -349,7 +363,9 @@ def _extend_step_group_steps(name: str, steps: StepList) -> StepList:
349
363
  return enter_step >> steps >> exit_step
350
364
 
351
365
 
352
- def step_group(name: str, steps: StepList, extract_form: bool = True) -> Step:
366
+ def step_group(
367
+ name: str, steps: StepList, extract_form: bool = True, retry_auth_callback: Authorizer | None = None
368
+ ) -> Step:
353
369
  """Add a group of steps to the workflow as a single step.
354
370
 
355
371
  A step group is a sequence of steps that act as a single step.
@@ -362,6 +378,7 @@ def step_group(name: str, steps: StepList, extract_form: bool = True) -> Step:
362
378
  name: The name of the step
363
379
  steps: The sub steps in the step group
364
380
  extract_form: Whether to attach the first form of the sub steps to the step group
381
+ retry_auth_callback: Callback to determine if user is authorized to retry this group on failure
365
382
  """
366
383
 
367
384
  steps = _extend_step_group_steps(name, steps)
@@ -392,7 +409,7 @@ def step_group(name: str, steps: StepList, extract_form: bool = True) -> Step:
392
409
 
393
410
  # Make sure we return a form is a sub step has a form
394
411
  form = next((sub_step.form for sub_step in steps if sub_step.form), None) if extract_form else None
395
- return make_step_function(func, name, form)
412
+ return make_step_function(func, name, form, retry_auth_callback=retry_auth_callback)
396
413
 
397
414
 
398
415
  def _create_endpoint_step(key: str = DEFAULT_CALLBACK_ROUTE_KEY) -> StepFunc:
@@ -16,7 +16,7 @@ from typing import Any
16
16
  import structlog
17
17
 
18
18
  from orchestrator.db import ProductTable
19
- from orchestrator.forms import FormPage
19
+ from orchestrator.forms import SubmitFormPage
20
20
  from orchestrator.forms.validators import Choice
21
21
  from orchestrator.services.subscriptions import (
22
22
  get_subscriptions_on_product_table_in_sync,
@@ -32,7 +32,7 @@ from pydantic_forms.types import FormGenerator, State
32
32
  logger = structlog.get_logger(__name__)
33
33
 
34
34
 
35
- def create_select_product_type_form() -> type[FormPage]:
35
+ def create_select_product_type_form() -> type[SubmitFormPage]:
36
36
  """Get and create the choices form for the product type."""
37
37
 
38
38
  @cache
@@ -41,7 +41,7 @@ def create_select_product_type_form() -> type[FormPage]:
41
41
 
42
42
  ProductTypeChoices = Choice.__call__("Product Type", get_product_type_choices())
43
43
 
44
- class SelectProductTypeForm(FormPage):
44
+ class SelectProductTypeForm(SubmitFormPage):
45
45
  product_type: ProductTypeChoices # type: ignore
46
46
 
47
47
  return SelectProductTypeForm
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestrator-core
3
- Version: 4.5.2
3
+ Version: 4.6.0
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.14
@@ -41,8 +41,8 @@ Requires-Dist: fastapi-etag==0.4.0
41
41
  Requires-Dist: itsdangerous>=2.2.0
42
42
  Requires-Dist: jinja2==3.1.6
43
43
  Requires-Dist: more-itertools~=10.7.0
44
- Requires-Dist: nwa-stdlib~=1.9.2
45
- Requires-Dist: oauth2-lib>=2.4.1
44
+ Requires-Dist: nwa-stdlib~=1.10.3
45
+ Requires-Dist: oauth2-lib>=2.5.0
46
46
  Requires-Dist: orjson==3.10.18
47
47
  Requires-Dist: pgvector>=0.4.1
48
48
  Requires-Dist: prometheus-client==0.22.1
@@ -55,7 +55,7 @@ Requires-Dist: python-rapidjson>=1.18,<1.21
55
55
  Requires-Dist: pytz==2025.2
56
56
  Requires-Dist: redis==5.1.1
57
57
  Requires-Dist: semver==3.0.4
58
- Requires-Dist: sentry-sdk[fastapi]~=2.29.1
58
+ Requires-Dist: sentry-sdk[fastapi]>=2.29.1
59
59
  Requires-Dist: sqlalchemy==2.0.41
60
60
  Requires-Dist: sqlalchemy-utils==0.41.2
61
61
  Requires-Dist: strawberry-graphql>=0.281.0
@@ -63,7 +63,7 @@ Requires-Dist: structlog>=25.4.0
63
63
  Requires-Dist: tabulate==0.9.0
64
64
  Requires-Dist: typer==0.15.4
65
65
  Requires-Dist: uvicorn[standard]~=0.34.0
66
- Requires-Dist: pydantic-ai-slim ==0.7.0 ; extra == "agent"
66
+ Requires-Dist: pydantic-ai-slim >=1.3.0 ; extra == "agent"
67
67
  Requires-Dist: ag-ui-protocol>=0.1.8 ; extra == "agent"
68
68
  Requires-Dist: litellm>=1.75.7 ; extra == "agent"
69
69
  Requires-Dist: celery~=5.5.1 ; extra == "celery"
@@ -119,21 +119,26 @@ Configure the database URI in your local environment:
119
119
  export DATABASE_URI=postgresql://nwa:nwa@localhost:5432/orchestrator-core
120
120
  ```
121
121
 
122
- ### Step 3 - Create main.py
122
+ ### Step 3 - Create main.py and wsgi.py
123
123
 
124
- Create a `main.py` file.
124
+ Create a `main.py` file for running the CLI.
125
125
 
126
126
  ```python
127
- from orchestrator import OrchestratorCore
128
127
  from orchestrator.cli.main import app as core_cli
129
- from orchestrator.settings import AppSettings
130
-
131
- app = OrchestratorCore(base_settings=AppSettings())
132
128
 
133
129
  if __name__ == "__main__":
134
130
  core_cli()
135
131
  ```
136
132
 
133
+ Create a `wsgi.py` file for running the web server.
134
+
135
+ ```python
136
+ from orchestrator import OrchestratorCore
137
+ from orchestrator.settings import AppSettings
138
+
139
+ app = OrchestratorCore(base_settings=AppSettings())
140
+ ```
141
+
137
142
  ### Step 4 - Run the database migrations
138
143
 
139
144
  Initialize the migration environment and database tables.
@@ -147,7 +152,7 @@ python main.py db upgrade heads
147
152
 
148
153
  ```shell
149
154
  export OAUTH2_ACTIVE=False
150
- uvicorn --reload --host 127.0.0.1 --port 8080 main:app
155
+ uvicorn --reload --host 127.0.0.1 --port 8080 wsgi:app
151
156
  ```
152
157
 
153
158
  Visit the [ReDoc](http://127.0.0.1:8080/api/redoc) or [OpenAPI](http://127.0.0.1:8080/api/docs) page to view and interact with the API.