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.
- julee/__init__.py +3 -0
- julee/api/__init__.py +20 -0
- julee/api/app.py +180 -0
- julee/api/dependencies.py +257 -0
- julee/api/requests.py +175 -0
- julee/api/responses.py +43 -0
- julee/api/routers/__init__.py +43 -0
- julee/api/routers/assembly_specifications.py +212 -0
- julee/api/routers/documents.py +182 -0
- julee/api/routers/knowledge_service_configs.py +79 -0
- julee/api/routers/knowledge_service_queries.py +293 -0
- julee/api/routers/system.py +137 -0
- julee/api/routers/workflows.py +234 -0
- julee/api/services/__init__.py +20 -0
- julee/api/services/system_initialization.py +214 -0
- julee/api/tests/__init__.py +14 -0
- julee/api/tests/routers/__init__.py +17 -0
- julee/api/tests/routers/test_assembly_specifications.py +749 -0
- julee/api/tests/routers/test_documents.py +301 -0
- julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
- julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
- julee/api/tests/routers/test_system.py +179 -0
- julee/api/tests/routers/test_workflows.py +393 -0
- julee/api/tests/test_app.py +285 -0
- julee/api/tests/test_dependencies.py +245 -0
- julee/api/tests/test_requests.py +250 -0
- julee/domain/__init__.py +22 -0
- julee/domain/models/__init__.py +49 -0
- julee/domain/models/assembly/__init__.py +17 -0
- julee/domain/models/assembly/assembly.py +103 -0
- julee/domain/models/assembly/tests/__init__.py +0 -0
- julee/domain/models/assembly/tests/factories.py +37 -0
- julee/domain/models/assembly/tests/test_assembly.py +430 -0
- julee/domain/models/assembly_specification/__init__.py +24 -0
- julee/domain/models/assembly_specification/assembly_specification.py +172 -0
- julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
- julee/domain/models/assembly_specification/tests/__init__.py +0 -0
- julee/domain/models/assembly_specification/tests/factories.py +78 -0
- julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
- julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
- julee/domain/models/custom_fields/__init__.py +0 -0
- julee/domain/models/custom_fields/content_stream.py +68 -0
- julee/domain/models/custom_fields/tests/__init__.py +0 -0
- julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
- julee/domain/models/document/__init__.py +17 -0
- julee/domain/models/document/document.py +150 -0
- julee/domain/models/document/tests/__init__.py +0 -0
- julee/domain/models/document/tests/factories.py +76 -0
- julee/domain/models/document/tests/test_document.py +297 -0
- julee/domain/models/knowledge_service_config/__init__.py +17 -0
- julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
- julee/domain/models/policy/__init__.py +15 -0
- julee/domain/models/policy/document_policy_validation.py +220 -0
- julee/domain/models/policy/policy.py +203 -0
- julee/domain/models/policy/tests/__init__.py +0 -0
- julee/domain/models/policy/tests/factories.py +47 -0
- julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
- julee/domain/models/policy/tests/test_policy.py +546 -0
- julee/domain/repositories/__init__.py +27 -0
- julee/domain/repositories/assembly.py +45 -0
- julee/domain/repositories/assembly_specification.py +52 -0
- julee/domain/repositories/base.py +146 -0
- julee/domain/repositories/document.py +49 -0
- julee/domain/repositories/document_policy_validation.py +52 -0
- julee/domain/repositories/knowledge_service_config.py +54 -0
- julee/domain/repositories/knowledge_service_query.py +44 -0
- julee/domain/repositories/policy.py +49 -0
- julee/domain/use_cases/__init__.py +17 -0
- julee/domain/use_cases/decorators.py +107 -0
- julee/domain/use_cases/extract_assemble_data.py +649 -0
- julee/domain/use_cases/initialize_system_data.py +842 -0
- julee/domain/use_cases/tests/__init__.py +7 -0
- julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
- julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
- julee/domain/use_cases/tests/test_validate_document.py +1228 -0
- julee/domain/use_cases/validate_document.py +736 -0
- julee/fixtures/assembly_specifications.yaml +70 -0
- julee/fixtures/documents.yaml +178 -0
- julee/fixtures/knowledge_service_configs.yaml +37 -0
- julee/fixtures/knowledge_service_queries.yaml +27 -0
- julee/repositories/__init__.py +17 -0
- julee/repositories/memory/__init__.py +31 -0
- julee/repositories/memory/assembly.py +84 -0
- julee/repositories/memory/assembly_specification.py +125 -0
- julee/repositories/memory/base.py +227 -0
- julee/repositories/memory/document.py +149 -0
- julee/repositories/memory/document_policy_validation.py +104 -0
- julee/repositories/memory/knowledge_service_config.py +123 -0
- julee/repositories/memory/knowledge_service_query.py +120 -0
- julee/repositories/memory/policy.py +87 -0
- julee/repositories/memory/tests/__init__.py +0 -0
- julee/repositories/memory/tests/test_document.py +212 -0
- julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
- julee/repositories/memory/tests/test_policy.py +443 -0
- julee/repositories/minio/__init__.py +31 -0
- julee/repositories/minio/assembly.py +103 -0
- julee/repositories/minio/assembly_specification.py +170 -0
- julee/repositories/minio/client.py +570 -0
- julee/repositories/minio/document.py +530 -0
- julee/repositories/minio/document_policy_validation.py +120 -0
- julee/repositories/minio/knowledge_service_config.py +187 -0
- julee/repositories/minio/knowledge_service_query.py +211 -0
- julee/repositories/minio/policy.py +106 -0
- julee/repositories/minio/tests/__init__.py +0 -0
- julee/repositories/minio/tests/fake_client.py +213 -0
- julee/repositories/minio/tests/test_assembly.py +374 -0
- julee/repositories/minio/tests/test_assembly_specification.py +391 -0
- julee/repositories/minio/tests/test_client_protocol.py +57 -0
- julee/repositories/minio/tests/test_document.py +591 -0
- julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
- julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
- julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
- julee/repositories/minio/tests/test_policy.py +559 -0
- julee/repositories/temporal/__init__.py +38 -0
- julee/repositories/temporal/activities.py +114 -0
- julee/repositories/temporal/activity_names.py +34 -0
- julee/repositories/temporal/proxies.py +159 -0
- julee/services/__init__.py +18 -0
- julee/services/knowledge_service/__init__.py +48 -0
- julee/services/knowledge_service/anthropic/__init__.py +12 -0
- julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
- julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
- julee/services/knowledge_service/factory.py +138 -0
- julee/services/knowledge_service/knowledge_service.py +160 -0
- julee/services/knowledge_service/memory/__init__.py +13 -0
- julee/services/knowledge_service/memory/knowledge_service.py +278 -0
- julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
- julee/services/knowledge_service/test_factory.py +112 -0
- julee/services/temporal/__init__.py +38 -0
- julee/services/temporal/activities.py +86 -0
- julee/services/temporal/activity_names.py +22 -0
- julee/services/temporal/proxies.py +41 -0
- julee/util/__init__.py +0 -0
- julee/util/domain.py +119 -0
- julee/util/repos/__init__.py +0 -0
- julee/util/repos/minio/__init__.py +0 -0
- julee/util/repos/minio/file_storage.py +213 -0
- julee/util/repos/temporal/__init__.py +11 -0
- julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
- julee/util/repos/temporal/data_converter.py +123 -0
- julee/util/repos/temporal/minio_file_storage.py +12 -0
- julee/util/repos/temporal/proxies/__init__.py +0 -0
- julee/util/repos/temporal/proxies/file_storage.py +58 -0
- julee/util/repositories.py +55 -0
- julee/util/temporal/__init__.py +22 -0
- julee/util/temporal/activities.py +123 -0
- julee/util/temporal/decorators.py +473 -0
- julee/util/tests/__init__.py +1 -0
- julee/util/tests/test_decorators.py +770 -0
- julee/util/validation/__init__.py +29 -0
- julee/util/validation/repository.py +100 -0
- julee/util/validation/type_guards.py +369 -0
- julee/worker.py +211 -0
- julee/workflows/__init__.py +26 -0
- julee/workflows/extract_assemble.py +215 -0
- julee/workflows/validate_document.py +228 -0
- julee-0.1.0.dist-info/METADATA +195 -0
- julee-0.1.0.dist-info/RECORD +161 -0
- julee-0.1.0.dist-info/WHEEL +5 -0
- julee-0.1.0.dist-info/licenses/LICENSE +674 -0
- 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
|
+
]
|