julee 0.1.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 (161) hide show
  1. julee/__init__.py +3 -0
  2. julee/api/__init__.py +20 -0
  3. julee/api/app.py +180 -0
  4. julee/api/dependencies.py +257 -0
  5. julee/api/requests.py +175 -0
  6. julee/api/responses.py +43 -0
  7. julee/api/routers/__init__.py +43 -0
  8. julee/api/routers/assembly_specifications.py +212 -0
  9. julee/api/routers/documents.py +182 -0
  10. julee/api/routers/knowledge_service_configs.py +79 -0
  11. julee/api/routers/knowledge_service_queries.py +293 -0
  12. julee/api/routers/system.py +137 -0
  13. julee/api/routers/workflows.py +234 -0
  14. julee/api/services/__init__.py +20 -0
  15. julee/api/services/system_initialization.py +214 -0
  16. julee/api/tests/__init__.py +14 -0
  17. julee/api/tests/routers/__init__.py +17 -0
  18. julee/api/tests/routers/test_assembly_specifications.py +749 -0
  19. julee/api/tests/routers/test_documents.py +301 -0
  20. julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
  21. julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
  22. julee/api/tests/routers/test_system.py +179 -0
  23. julee/api/tests/routers/test_workflows.py +393 -0
  24. julee/api/tests/test_app.py +285 -0
  25. julee/api/tests/test_dependencies.py +245 -0
  26. julee/api/tests/test_requests.py +250 -0
  27. julee/domain/__init__.py +22 -0
  28. julee/domain/models/__init__.py +49 -0
  29. julee/domain/models/assembly/__init__.py +17 -0
  30. julee/domain/models/assembly/assembly.py +103 -0
  31. julee/domain/models/assembly/tests/__init__.py +0 -0
  32. julee/domain/models/assembly/tests/factories.py +37 -0
  33. julee/domain/models/assembly/tests/test_assembly.py +430 -0
  34. julee/domain/models/assembly_specification/__init__.py +24 -0
  35. julee/domain/models/assembly_specification/assembly_specification.py +172 -0
  36. julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
  37. julee/domain/models/assembly_specification/tests/__init__.py +0 -0
  38. julee/domain/models/assembly_specification/tests/factories.py +78 -0
  39. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
  40. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
  41. julee/domain/models/custom_fields/__init__.py +0 -0
  42. julee/domain/models/custom_fields/content_stream.py +68 -0
  43. julee/domain/models/custom_fields/tests/__init__.py +0 -0
  44. julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
  45. julee/domain/models/document/__init__.py +17 -0
  46. julee/domain/models/document/document.py +150 -0
  47. julee/domain/models/document/tests/__init__.py +0 -0
  48. julee/domain/models/document/tests/factories.py +76 -0
  49. julee/domain/models/document/tests/test_document.py +297 -0
  50. julee/domain/models/knowledge_service_config/__init__.py +17 -0
  51. julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
  52. julee/domain/models/policy/__init__.py +15 -0
  53. julee/domain/models/policy/document_policy_validation.py +220 -0
  54. julee/domain/models/policy/policy.py +203 -0
  55. julee/domain/models/policy/tests/__init__.py +0 -0
  56. julee/domain/models/policy/tests/factories.py +47 -0
  57. julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
  58. julee/domain/models/policy/tests/test_policy.py +546 -0
  59. julee/domain/repositories/__init__.py +27 -0
  60. julee/domain/repositories/assembly.py +45 -0
  61. julee/domain/repositories/assembly_specification.py +52 -0
  62. julee/domain/repositories/base.py +146 -0
  63. julee/domain/repositories/document.py +49 -0
  64. julee/domain/repositories/document_policy_validation.py +52 -0
  65. julee/domain/repositories/knowledge_service_config.py +54 -0
  66. julee/domain/repositories/knowledge_service_query.py +44 -0
  67. julee/domain/repositories/policy.py +49 -0
  68. julee/domain/use_cases/__init__.py +17 -0
  69. julee/domain/use_cases/decorators.py +107 -0
  70. julee/domain/use_cases/extract_assemble_data.py +649 -0
  71. julee/domain/use_cases/initialize_system_data.py +842 -0
  72. julee/domain/use_cases/tests/__init__.py +7 -0
  73. julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
  74. julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
  75. julee/domain/use_cases/tests/test_validate_document.py +1228 -0
  76. julee/domain/use_cases/validate_document.py +736 -0
  77. julee/fixtures/assembly_specifications.yaml +70 -0
  78. julee/fixtures/documents.yaml +178 -0
  79. julee/fixtures/knowledge_service_configs.yaml +37 -0
  80. julee/fixtures/knowledge_service_queries.yaml +27 -0
  81. julee/repositories/__init__.py +17 -0
  82. julee/repositories/memory/__init__.py +31 -0
  83. julee/repositories/memory/assembly.py +84 -0
  84. julee/repositories/memory/assembly_specification.py +125 -0
  85. julee/repositories/memory/base.py +227 -0
  86. julee/repositories/memory/document.py +149 -0
  87. julee/repositories/memory/document_policy_validation.py +104 -0
  88. julee/repositories/memory/knowledge_service_config.py +123 -0
  89. julee/repositories/memory/knowledge_service_query.py +120 -0
  90. julee/repositories/memory/policy.py +87 -0
  91. julee/repositories/memory/tests/__init__.py +0 -0
  92. julee/repositories/memory/tests/test_document.py +212 -0
  93. julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
  94. julee/repositories/memory/tests/test_policy.py +443 -0
  95. julee/repositories/minio/__init__.py +31 -0
  96. julee/repositories/minio/assembly.py +103 -0
  97. julee/repositories/minio/assembly_specification.py +170 -0
  98. julee/repositories/minio/client.py +570 -0
  99. julee/repositories/minio/document.py +530 -0
  100. julee/repositories/minio/document_policy_validation.py +120 -0
  101. julee/repositories/minio/knowledge_service_config.py +187 -0
  102. julee/repositories/minio/knowledge_service_query.py +211 -0
  103. julee/repositories/minio/policy.py +106 -0
  104. julee/repositories/minio/tests/__init__.py +0 -0
  105. julee/repositories/minio/tests/fake_client.py +213 -0
  106. julee/repositories/minio/tests/test_assembly.py +374 -0
  107. julee/repositories/minio/tests/test_assembly_specification.py +391 -0
  108. julee/repositories/minio/tests/test_client_protocol.py +57 -0
  109. julee/repositories/minio/tests/test_document.py +591 -0
  110. julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
  111. julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
  112. julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
  113. julee/repositories/minio/tests/test_policy.py +559 -0
  114. julee/repositories/temporal/__init__.py +38 -0
  115. julee/repositories/temporal/activities.py +114 -0
  116. julee/repositories/temporal/activity_names.py +34 -0
  117. julee/repositories/temporal/proxies.py +159 -0
  118. julee/services/__init__.py +18 -0
  119. julee/services/knowledge_service/__init__.py +48 -0
  120. julee/services/knowledge_service/anthropic/__init__.py +12 -0
  121. julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
  122. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
  123. julee/services/knowledge_service/factory.py +138 -0
  124. julee/services/knowledge_service/knowledge_service.py +160 -0
  125. julee/services/knowledge_service/memory/__init__.py +13 -0
  126. julee/services/knowledge_service/memory/knowledge_service.py +278 -0
  127. julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
  128. julee/services/knowledge_service/test_factory.py +112 -0
  129. julee/services/temporal/__init__.py +38 -0
  130. julee/services/temporal/activities.py +86 -0
  131. julee/services/temporal/activity_names.py +22 -0
  132. julee/services/temporal/proxies.py +41 -0
  133. julee/util/__init__.py +0 -0
  134. julee/util/domain.py +119 -0
  135. julee/util/repos/__init__.py +0 -0
  136. julee/util/repos/minio/__init__.py +0 -0
  137. julee/util/repos/minio/file_storage.py +213 -0
  138. julee/util/repos/temporal/__init__.py +11 -0
  139. julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
  140. julee/util/repos/temporal/data_converter.py +123 -0
  141. julee/util/repos/temporal/minio_file_storage.py +12 -0
  142. julee/util/repos/temporal/proxies/__init__.py +0 -0
  143. julee/util/repos/temporal/proxies/file_storage.py +58 -0
  144. julee/util/repositories.py +55 -0
  145. julee/util/temporal/__init__.py +22 -0
  146. julee/util/temporal/activities.py +123 -0
  147. julee/util/temporal/decorators.py +473 -0
  148. julee/util/tests/__init__.py +1 -0
  149. julee/util/tests/test_decorators.py +770 -0
  150. julee/util/validation/__init__.py +29 -0
  151. julee/util/validation/repository.py +100 -0
  152. julee/util/validation/type_guards.py +369 -0
  153. julee/worker.py +211 -0
  154. julee/workflows/__init__.py +26 -0
  155. julee/workflows/extract_assemble.py +215 -0
  156. julee/workflows/validate_document.py +228 -0
  157. julee-0.1.0.dist-info/METADATA +195 -0
  158. julee-0.1.0.dist-info/RECORD +161 -0
  159. julee-0.1.0.dist-info/WHEEL +5 -0
  160. julee-0.1.0.dist-info/licenses/LICENSE +674 -0
  161. julee-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,293 @@
1
+ """
2
+ Knowledge Service Queries API router for the julee CEAP system.
3
+
4
+ This module provides the API endpoints for knowledge service queries,
5
+ which define how to extract specific data using external knowledge services
6
+ during the assembly process.
7
+
8
+ Routes defined at root level:
9
+ - GET / - List knowledge service queries (paginated)
10
+ - GET /{query_id} - Get individual query details
11
+ - POST / - Create new knowledge service query
12
+
13
+ These routes are mounted at /knowledge_service_queries in the main app.
14
+ """
15
+
16
+ import logging
17
+ from typing import Optional, cast
18
+ from fastapi import APIRouter, Depends, HTTPException, Query
19
+ from fastapi_pagination import Page, paginate
20
+
21
+ from julee.domain.models import KnowledgeServiceQuery
22
+ from julee.domain.repositories.knowledge_service_query import (
23
+ KnowledgeServiceQueryRepository,
24
+ )
25
+ from julee.api.dependencies import (
26
+ get_knowledge_service_query_repository,
27
+ )
28
+ from julee.api.requests import CreateKnowledgeServiceQueryRequest
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Create the router for knowledge service queries
33
+ router = APIRouter()
34
+
35
+
36
+ @router.get("/", response_model=Page[KnowledgeServiceQuery])
37
+ async def get_knowledge_service_queries(
38
+ ids: Optional[str] = Query(
39
+ None,
40
+ description="Comma-separated list of query IDs for bulk retrieval",
41
+ openapi_examples={
42
+ "bulk_query": {
43
+ "summary": "Bulk retrieval example",
44
+ "value": "query-123,query-456,query-789",
45
+ }
46
+ },
47
+ ),
48
+ repository: KnowledgeServiceQueryRepository = Depends( # type: ignore[misc]
49
+ get_knowledge_service_query_repository
50
+ ),
51
+ ) -> Page[KnowledgeServiceQuery]:
52
+ """
53
+ Get knowledge service queries by IDs or list all with pagination.
54
+
55
+ This endpoint supports two modes:
56
+ 1. Bulk retrieval: Pass comma-separated IDs to get specific queries
57
+ 2. List all: Without IDs parameter, returns paginated list of all queries
58
+
59
+ Each query contains the configuration needed to extract specific data
60
+ using external knowledge services.
61
+
62
+ Args:
63
+ ids: Optional comma-separated list of query IDs for bulk retrieval
64
+
65
+ Returns:
66
+ Page[KnowledgeServiceQuery]: List of queries (bulk) or paginated
67
+ list (all)
68
+ """
69
+ if ids is not None:
70
+ # Check for empty or whitespace-only parameter
71
+ if not ids.strip():
72
+ raise HTTPException(
73
+ status_code=400,
74
+ detail="Invalid ids parameter: must contain at least one " "valid ID",
75
+ )
76
+
77
+ # Bulk retrieval mode
78
+ logger.info(
79
+ "Bulk knowledge service queries requested",
80
+ extra={"ids_param": ids},
81
+ )
82
+
83
+ try:
84
+ # Parse and validate IDs
85
+ id_list = [id.strip() for id in ids.split(",") if id.strip()]
86
+ if not id_list:
87
+ raise HTTPException(
88
+ status_code=400,
89
+ detail="Invalid ids parameter: must contain at least "
90
+ "one valid ID",
91
+ )
92
+
93
+ if len(id_list) > 100: # Reasonable limit
94
+ raise HTTPException(
95
+ status_code=400,
96
+ detail="Too many IDs requested: maximum 100 IDs per " "request",
97
+ )
98
+
99
+ # Use repository's get_many method
100
+ results = await repository.get_many(id_list)
101
+
102
+ # Filter out None results and preserve found queries
103
+ found_queries = [query for query in results.values() if query is not None]
104
+
105
+ logger.info(
106
+ "Bulk knowledge service queries retrieved successfully",
107
+ extra={
108
+ "requested_count": len(id_list),
109
+ "found_count": len(found_queries),
110
+ "missing_count": len(id_list) - len(found_queries),
111
+ },
112
+ )
113
+
114
+ # Return as paginated result for consistent API response format
115
+ return cast(Page[KnowledgeServiceQuery], paginate(found_queries))
116
+
117
+ except HTTPException:
118
+ # Re-raise HTTP exceptions (like 400 Bad Request)
119
+ raise
120
+ except Exception as e:
121
+ logger.error(
122
+ "Failed to retrieve bulk knowledge service queries",
123
+ exc_info=True,
124
+ extra={
125
+ "error_type": type(e).__name__,
126
+ "error_message": str(e),
127
+ "ids_param": ids,
128
+ },
129
+ )
130
+ raise HTTPException(
131
+ status_code=500,
132
+ detail="Failed to retrieve queries due to an internal error.",
133
+ )
134
+ else:
135
+ # List all mode (existing functionality)
136
+ logger.info("All knowledge service queries requested")
137
+
138
+ try:
139
+ # Get all knowledge service queries from the repository
140
+ queries = await repository.list_all()
141
+
142
+ logger.info(
143
+ "Knowledge service queries retrieved successfully",
144
+ extra={"count": len(queries)},
145
+ )
146
+
147
+ # Use fastapi-pagination to paginate the results
148
+ return cast(Page[KnowledgeServiceQuery], paginate(queries))
149
+
150
+ except Exception as e:
151
+ logger.error(
152
+ "Failed to retrieve knowledge service queries",
153
+ exc_info=True,
154
+ extra={
155
+ "error_type": type(e).__name__,
156
+ "error_message": str(e),
157
+ },
158
+ )
159
+ raise HTTPException(
160
+ status_code=500,
161
+ detail="Failed to retrieve queries due to an internal error.",
162
+ )
163
+
164
+
165
+ @router.post("/", response_model=KnowledgeServiceQuery)
166
+ async def create_knowledge_service_query(
167
+ request: CreateKnowledgeServiceQueryRequest,
168
+ repository: KnowledgeServiceQueryRepository = Depends( # type: ignore[misc]
169
+ get_knowledge_service_query_repository
170
+ ),
171
+ ) -> KnowledgeServiceQuery:
172
+ """
173
+ Create a new knowledge service query.
174
+
175
+ This endpoint creates a new knowledge service query configuration that
176
+ defines how to extract specific data using external knowledge services
177
+ during the assembly process.
178
+
179
+ Args:
180
+ request: The knowledge service query creation request
181
+ repository: Injected repository for persistence
182
+
183
+ Returns:
184
+ KnowledgeServiceQuery: The created query with generated ID and
185
+ timestamps
186
+ """
187
+ logger.info(
188
+ "Knowledge service query creation requested",
189
+ extra={"query_name": request.name},
190
+ )
191
+
192
+ try:
193
+ # Generate unique ID for the new query
194
+ query_id = await repository.generate_id()
195
+
196
+ # Convert request to domain model with generated ID
197
+ query = request.to_domain_model(query_id)
198
+
199
+ # Save the query via repository
200
+ await repository.save(query)
201
+
202
+ logger.info(
203
+ "Knowledge service query created successfully",
204
+ extra={
205
+ "query_id": query.query_id,
206
+ "query_name": query.name,
207
+ "knowledge_service_id": query.knowledge_service_id,
208
+ },
209
+ )
210
+
211
+ return query
212
+
213
+ except Exception as e:
214
+ logger.error(
215
+ "Failed to create knowledge service query",
216
+ exc_info=True,
217
+ extra={
218
+ "error_type": type(e).__name__,
219
+ "error_message": str(e),
220
+ "query_name": request.name,
221
+ },
222
+ )
223
+ raise HTTPException(
224
+ status_code=500,
225
+ detail="Failed to create query due to an internal error.",
226
+ )
227
+
228
+
229
+ @router.get("/{query_id}", response_model=KnowledgeServiceQuery)
230
+ async def get_knowledge_service_query(
231
+ query_id: str,
232
+ repository: KnowledgeServiceQueryRepository = Depends( # type: ignore[misc]
233
+ get_knowledge_service_query_repository
234
+ ),
235
+ ) -> KnowledgeServiceQuery:
236
+ """
237
+ Get a specific knowledge service query by ID.
238
+
239
+ Args:
240
+ query_id: The ID of the query to retrieve
241
+ repository: Injected repository for data access
242
+
243
+ Returns:
244
+ KnowledgeServiceQuery: The requested query
245
+
246
+ Raises:
247
+ HTTPException: 404 if query not found, 500 for internal errors
248
+ """
249
+ logger.info(
250
+ "Knowledge service query detail requested",
251
+ extra={"query_id": query_id},
252
+ )
253
+
254
+ try:
255
+ query = await repository.get(query_id)
256
+
257
+ if query is None:
258
+ logger.warning(
259
+ "Knowledge service query not found",
260
+ extra={"query_id": query_id},
261
+ )
262
+ raise HTTPException(
263
+ status_code=404,
264
+ detail=f"Knowledge service query with ID '{query_id}' " "not found",
265
+ )
266
+
267
+ logger.info(
268
+ "Knowledge service query retrieved successfully",
269
+ extra={
270
+ "query_id": query.query_id,
271
+ "query_name": query.name,
272
+ },
273
+ )
274
+
275
+ return query
276
+
277
+ except HTTPException:
278
+ # Re-raise HTTP exceptions (like 404 Not Found)
279
+ raise
280
+ except Exception as e:
281
+ logger.error(
282
+ "Failed to retrieve knowledge service query",
283
+ exc_info=True,
284
+ extra={
285
+ "error_type": type(e).__name__,
286
+ "error_message": str(e),
287
+ "query_id": query_id,
288
+ },
289
+ )
290
+ raise HTTPException(
291
+ status_code=500,
292
+ detail="Failed to retrieve query due to an internal error.",
293
+ )
@@ -0,0 +1,137 @@
1
+ """
2
+ System API router for the julee CEAP system.
3
+
4
+ This module provides system-level API endpoints including health checks,
5
+ status information, and other operational endpoints.
6
+
7
+ Routes defined at root level:
8
+ - GET /health - Health check endpoint
9
+
10
+ These routes are mounted at the root level in the main app.
11
+ """
12
+
13
+ import logging
14
+ import asyncio
15
+ from datetime import datetime, timezone
16
+ from fastapi import APIRouter
17
+ from temporalio.client import Client
18
+ from minio import Minio
19
+ import os
20
+
21
+ from julee.api.responses import (
22
+ HealthCheckResponse,
23
+ ServiceHealthStatus,
24
+ ServiceStatus,
25
+ SystemStatus,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Create the router for system endpoints
31
+ router = APIRouter()
32
+
33
+
34
+ async def check_temporal_health() -> ServiceStatus:
35
+ """Check if Temporal service is available."""
36
+ try:
37
+ # Get Temporal server address from environment or use default
38
+ temporal_address = os.getenv(
39
+ "TEMPORAL_ENDPOINT", os.getenv("TEMPORAL_HOST", "localhost:7233")
40
+ )
41
+
42
+ # Create a client and try to connect
43
+ _ = await Client.connect(temporal_address, namespace="default")
44
+ # Simple check - if we can connect, assume it's working
45
+ return ServiceStatus.UP
46
+ except Exception as e:
47
+ logger.warning("Temporal health check failed: %s", e)
48
+ return ServiceStatus.DOWN
49
+
50
+
51
+ async def check_storage_health() -> ServiceStatus:
52
+ """Check if storage service (Minio) is available."""
53
+ try:
54
+ # Get Minio configuration (prioritize Docker network address)
55
+ endpoint = os.environ.get("MINIO_ENDPOINT", "localhost:9000")
56
+ access_key = os.environ.get("MINIO_ACCESS_KEY", "minioadmin")
57
+ secret_key = os.environ.get("MINIO_SECRET_KEY", "minioadmin")
58
+ secure = os.environ.get("MINIO_SECURE", "false").lower() == "true"
59
+
60
+ # Create Minio client
61
+ client = Minio(
62
+ endpoint=endpoint,
63
+ access_key=access_key,
64
+ secret_key=secret_key,
65
+ secure=secure,
66
+ )
67
+
68
+ # Test connection by listing buckets
69
+ _ = list(client.list_buckets())
70
+ return ServiceStatus.UP
71
+ except Exception as e:
72
+ logger.warning("Storage health check failed: %s", e)
73
+ return ServiceStatus.DOWN
74
+
75
+
76
+ async def check_api_health() -> ServiceStatus:
77
+ """Check if API service is available (self-check)."""
78
+ # Since we're responding, API is up
79
+ return ServiceStatus.UP
80
+
81
+
82
+ def determine_overall_status(services: ServiceHealthStatus) -> SystemStatus:
83
+ """Determine overall system status based on service statuses."""
84
+ service_statuses = [services.api, services.temporal, services.storage]
85
+
86
+ if all(status == ServiceStatus.UP for status in service_statuses):
87
+ return SystemStatus.HEALTHY
88
+ elif any(status == ServiceStatus.UP for status in service_statuses):
89
+ return SystemStatus.DEGRADED
90
+ else:
91
+ return SystemStatus.UNHEALTHY
92
+
93
+
94
+ @router.get("/health", response_model=HealthCheckResponse)
95
+ async def health_check() -> HealthCheckResponse:
96
+ """Comprehensive health check endpoint that checks all services."""
97
+ logger.info("Performing health check")
98
+
99
+ # Check all services concurrently
100
+ results = await asyncio.gather(
101
+ check_api_health(),
102
+ check_temporal_health(),
103
+ check_storage_health(),
104
+ return_exceptions=True,
105
+ )
106
+
107
+ # Handle any exceptions from the health checks
108
+ api_status = results[0]
109
+ temporal_status = results[1]
110
+ storage_status = results[2]
111
+
112
+ if isinstance(api_status, Exception):
113
+ logger.error("API health check error: %s", api_status)
114
+ api_status = ServiceStatus.DOWN
115
+ if isinstance(temporal_status, Exception):
116
+ logger.error("Temporal health check error: %s", temporal_status)
117
+ temporal_status = ServiceStatus.DOWN
118
+ if isinstance(storage_status, Exception):
119
+ logger.error("Storage health check error: %s", storage_status)
120
+ storage_status = ServiceStatus.DOWN
121
+
122
+ # Create service health status with proper typing
123
+ services = ServiceHealthStatus(
124
+ api=ServiceStatus(api_status),
125
+ temporal=ServiceStatus(temporal_status),
126
+ storage=ServiceStatus(storage_status),
127
+ )
128
+
129
+ # Determine overall status
130
+ overall_status = determine_overall_status(services)
131
+
132
+ # Return response with string timestamp as expected by frontend
133
+ return HealthCheckResponse(
134
+ status=overall_status,
135
+ timestamp=datetime.now(timezone.utc).isoformat(),
136
+ services=services,
137
+ )
@@ -0,0 +1,234 @@
1
+ """
2
+ Workflows API router for the julee CEAP system.
3
+
4
+ This module provides workflow management API endpoints for starting,
5
+ monitoring, and managing workflows in the system.
6
+
7
+ Routes defined at root level:
8
+ - POST /extract-assemble - Start extract-assemble workflow
9
+ - GET /{workflow_id}/status - Get workflow status
10
+ - GET / - List workflows
11
+
12
+ These routes are mounted with '/workflows' prefix in the main app.
13
+ """
14
+
15
+ import logging
16
+ import uuid
17
+ from typing import Optional
18
+
19
+ from fastapi import APIRouter, Depends, HTTPException
20
+ from pydantic import BaseModel, Field
21
+ from temporalio.client import Client
22
+
23
+ from julee.api.dependencies import get_temporal_client
24
+ from julee.workflows.extract_assemble import (
25
+ ExtractAssembleWorkflow,
26
+ EXTRACT_ASSEMBLE_RETRY_POLICY,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ router = APIRouter()
32
+
33
+
34
+ class StartExtractAssembleRequest(BaseModel):
35
+ """Request model for starting extract-assemble workflow."""
36
+
37
+ document_id: str = Field(..., min_length=1, description="Document ID to process")
38
+ assembly_specification_id: str = Field(
39
+ ..., min_length=1, description="Assembly specification ID to use"
40
+ )
41
+ workflow_id: Optional[str] = Field(
42
+ None,
43
+ min_length=1,
44
+ description=("Optional custom workflow ID (auto-generated if not provided)"),
45
+ )
46
+
47
+
48
+ class WorkflowStatusResponse(BaseModel):
49
+ """Response model for workflow status."""
50
+
51
+ workflow_id: str
52
+ run_id: str
53
+ status: str # "RUNNING", "COMPLETED", "FAILED", "CANCELLED", etc.
54
+ current_step: Optional[str] = None
55
+ assembly_id: Optional[str] = None
56
+
57
+
58
+ class StartWorkflowResponse(BaseModel):
59
+ """Response model for starting a workflow."""
60
+
61
+ workflow_id: str
62
+ run_id: str
63
+ status: str
64
+ message: str
65
+
66
+
67
+ @router.post("/extract-assemble", response_model=StartWorkflowResponse)
68
+ async def start_extract_assemble_workflow(
69
+ request: StartExtractAssembleRequest,
70
+ temporal_client: Client = Depends(get_temporal_client),
71
+ ) -> StartWorkflowResponse:
72
+ """
73
+ Start an extract-assemble workflow.
74
+
75
+ Args:
76
+ request: Workflow start request with document and spec IDs
77
+ temporal_client: Temporal client dependency
78
+
79
+ Returns:
80
+ Workflow ID and initial status
81
+
82
+ Raises:
83
+ HTTPException: If workflow start fails
84
+ """
85
+ try:
86
+ logger.info("Starting extract-assemble workflow request received")
87
+
88
+ # Generate workflow ID if not provided
89
+ workflow_id = request.workflow_id
90
+ if not workflow_id:
91
+ workflow_id = (
92
+ f"extract-assemble-{request.document_id}-"
93
+ f"{request.assembly_specification_id}-{uuid.uuid4().hex[:8]}"
94
+ )
95
+
96
+ logger.info(
97
+ "Starting ExtractAssemble workflow",
98
+ extra={
99
+ "workflow_id": workflow_id,
100
+ "document_id": request.document_id,
101
+ "assembly_specification_id": (request.assembly_specification_id),
102
+ },
103
+ )
104
+
105
+ # Start the workflow
106
+ handle = await temporal_client.start_workflow(
107
+ ExtractAssembleWorkflow.run,
108
+ args=[request.document_id, request.assembly_specification_id],
109
+ id=workflow_id,
110
+ task_queue="julee-extract-assemble-queue",
111
+ retry_policy=EXTRACT_ASSEMBLE_RETRY_POLICY,
112
+ )
113
+
114
+ logger.info(
115
+ "ExtractAssemble workflow started successfully",
116
+ extra={
117
+ "workflow_id": workflow_id,
118
+ "run_id": handle.run_id,
119
+ },
120
+ )
121
+
122
+ return StartWorkflowResponse(
123
+ workflow_id=workflow_id,
124
+ run_id=handle.run_id or "unknown",
125
+ status="RUNNING",
126
+ message="Workflow started successfully",
127
+ )
128
+
129
+ except Exception as e:
130
+ logger.error(
131
+ "Failed to start extract-assemble workflow: %s",
132
+ e,
133
+ extra={
134
+ "document_id": request.document_id,
135
+ "assembly_specification_id": (request.assembly_specification_id),
136
+ },
137
+ )
138
+ raise HTTPException(status_code=500, detail="Failed to start workflow") from e
139
+
140
+
141
+ @router.get("/{workflow_id}/status", response_model=WorkflowStatusResponse)
142
+ async def get_workflow_status(
143
+ workflow_id: str,
144
+ temporal_client: Client = Depends(get_temporal_client),
145
+ ) -> WorkflowStatusResponse:
146
+ """
147
+ Get the status of a workflow.
148
+
149
+ Args:
150
+ workflow_id: Workflow ID to query
151
+ temporal_client: Temporal client dependency
152
+
153
+ Returns:
154
+ Current workflow status and details
155
+
156
+ Raises:
157
+ HTTPException: If workflow not found or query fails
158
+ """
159
+ logger.info("Getting workflow status", extra={"workflow_id": workflow_id})
160
+
161
+ # Get workflow handle - if this fails, workflow doesn't exist
162
+ try:
163
+ handle = temporal_client.get_workflow_handle(workflow_id)
164
+ except Exception as e:
165
+ # Check if it's a workflow not found error (common patterns)
166
+ error_message = str(e).lower()
167
+ if any(
168
+ pattern in error_message
169
+ for pattern in [
170
+ "not found",
171
+ "notfound",
172
+ "does not exist",
173
+ "workflow_not_found",
174
+ ]
175
+ ):
176
+ raise HTTPException(
177
+ status_code=404,
178
+ detail=f"Workflow with ID '{workflow_id}' not found",
179
+ )
180
+
181
+ # Other errors from getting workflow handle
182
+ logger.error(
183
+ "Failed to get workflow handle: %s",
184
+ e,
185
+ extra={"workflow_id": workflow_id},
186
+ )
187
+ raise HTTPException(
188
+ status_code=500, detail="Failed to retrieve workflow handle"
189
+ ) from e
190
+
191
+ # Get workflow description - if this fails, it's a server error
192
+ try:
193
+ description = await handle.describe()
194
+ except Exception as e:
195
+ logger.error(
196
+ "Failed to describe workflow: %s",
197
+ e,
198
+ extra={"workflow_id": workflow_id},
199
+ )
200
+ raise HTTPException(
201
+ status_code=500, detail="Failed to retrieve workflow description"
202
+ ) from e
203
+
204
+ # Query current step and assembly ID if workflow supports it
205
+ current_step = None
206
+ assembly_id = None
207
+ try:
208
+ current_step = await handle.query("get_current_step")
209
+ assembly_id = await handle.query("get_assembly_id")
210
+ except Exception as query_error:
211
+ logger.debug(
212
+ "Could not query workflow details: %s",
213
+ query_error,
214
+ extra={"workflow_id": workflow_id},
215
+ )
216
+
217
+ status_response = WorkflowStatusResponse(
218
+ workflow_id=workflow_id,
219
+ run_id=description.run_id or "unknown",
220
+ status=description.status.name if description.status else "UNKNOWN",
221
+ current_step=current_step,
222
+ assembly_id=assembly_id,
223
+ )
224
+
225
+ logger.info(
226
+ "Retrieved workflow status",
227
+ extra={
228
+ "workflow_id": workflow_id,
229
+ "status": status_response.status,
230
+ "current_step": current_step,
231
+ },
232
+ )
233
+
234
+ return status_response
@@ -0,0 +1,20 @@
1
+ """
2
+ API services package for the julee CEAP system.
3
+
4
+ This package contains service layer components that orchestrate use cases
5
+ and provide higher-level application services. Services in this package
6
+ act as facades between the API layer and the domain layer, coordinating
7
+ multiple use cases and handling cross-cutting concerns.
8
+
9
+ Services follow clean architecture principles:
10
+ - Orchestrate domain use cases
11
+ - Handle application-level concerns
12
+ - Provide simplified interfaces for controllers
13
+ - Maintain separation between API and domain layers
14
+ """
15
+
16
+ from .system_initialization import SystemInitializationService
17
+
18
+ __all__ = [
19
+ "SystemInitializationService",
20
+ ]