redhairedshanks1 commited on
Commit
a955a4b
Β·
1 Parent(s): 52f4641

Update services/pipeline_generator.py

Browse files
Files changed (1) hide show
  1. services/pipeline_generator.py +410 -410
services/pipeline_generator.py CHANGED
@@ -1,410 +1,410 @@
1
- # services/pipeline_generator.py
2
- """
3
- Unified pipeline generator with Bedrock (priority) and Gemini (fallback)
4
- """
5
- import json
6
- import os
7
- import re
8
- from typing import Dict, Any, List, Optional
9
- from pydantic import BaseModel, Field
10
-
11
- # For Bedrock
12
- try:
13
- from langchain_aws import ChatBedrock
14
- from langchain_core.prompts import ChatPromptTemplate
15
- BEDROCK_AVAILABLE = True
16
- except ImportError:
17
- BEDROCK_AVAILABLE = False
18
- print("Warning: langchain_aws not available, Bedrock will be disabled")
19
-
20
- # For Gemini
21
- import requests
22
-
23
-
24
- # ========================
25
- # PYDANTIC MODELS
26
- # ========================
27
-
28
- class ComponentConfig(BaseModel):
29
- """Configuration for a single pipeline component"""
30
- tool_name: str = Field(description="Name of the tool to execute")
31
- start_page: int = Field(default=1, description="Starting page number (1-indexed)")
32
- end_page: int = Field(default=1, description="Ending page number (inclusive)")
33
- params: Dict[str, Any] = Field(default_factory=dict, description="Additional tool-specific parameters")
34
-
35
- class PipelineConfig(BaseModel):
36
- """Complete pipeline configuration"""
37
- pipeline_name: str = Field(description="Name/identifier for the pipeline")
38
- components: List[ComponentConfig] = Field(description="Ordered list of components to execute")
39
- target_lang: Optional[str] = Field(default=None, description="Target language for translation (if applicable)")
40
- reason: str = Field(description="AI's reasoning for this pipeline structure")
41
- metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
42
-
43
-
44
- # ========================
45
- # BEDROCK PIPELINE GENERATOR
46
- # ========================
47
-
48
- def generate_pipeline_bedrock(user_input: str, file_path: Optional[str] = None) -> Dict[str, Any]:
49
- """
50
- Generate pipeline using AWS Bedrock (Claude 3.5 Sonnet)
51
- Priority method - tries this first
52
- """
53
- if not BEDROCK_AVAILABLE:
54
- raise RuntimeError("Bedrock not available - langchain_aws not installed")
55
-
56
- # Check for AWS credentials
57
- if not os.getenv("AWS_ACCESS_KEY_ID") or not os.getenv("AWS_SECRET_ACCESS_KEY"):
58
- raise RuntimeError("AWS credentials not configured")
59
-
60
- try:
61
- llm = ChatBedrock(
62
- model_id=os.getenv("BEDROCK_MODEL", "anthropic.claude-3-5-sonnet-20241022-v2:0"),
63
- region_name=os.getenv("AWS_REGION", "us-east-1"),
64
- temperature=0.0,
65
- )
66
-
67
- prompt = ChatPromptTemplate.from_messages([
68
- ("system", """You are a document processing pipeline expert. Generate a detailed pipeline plan.
69
-
70
- Available tools and their parameters:
71
- 1. extract_text - Extract text from documents
72
- - start_page (int): Starting page number
73
- - end_page (int): Ending page number
74
- - params: {{"encoding": "utf-8", "preserve_layout": bool}}
75
-
76
- 2. extract_tables - Extract tables from documents
77
- - start_page (int): Starting page number
78
- - end_page (int): Ending page number
79
- - params: {{"format": "json"|"csv", "include_headers": bool}}
80
-
81
- 3. describe_images - Generate image descriptions
82
- - start_page (int): Starting page number
83
- - end_page (int): Ending page number
84
- - params: {{"detail_level": "low"|"medium"|"high"}}
85
-
86
- 4. summarize_text - Summarize extracted text
87
- - No page range (works on extracted text)
88
- - params: {{"max_length": int, "style": "concise"|"detailed"}}
89
-
90
- 5. classify_text - Classify document content
91
- - No page range (works on extracted text)
92
- - params: {{"categories": list[str]}}
93
-
94
- 6. extract_entities - Named Entity Recognition
95
- - No page range (works on extracted text)
96
- - params: {{"entity_types": list[str]}}
97
-
98
- 7. translate_text - Translate text to target language
99
- - No page range (works on extracted text)
100
- - params: {{"target_lang": str, "source_lang": str}}
101
-
102
- 8. signature_verification - Verify signatures
103
- - start_page (int): Starting page number
104
- - end_page (int): Ending page number
105
- - params: {{}}
106
-
107
- 9. stamp_detection - Detect stamps
108
- - start_page (int): Starting page number
109
- - end_page (int): Ending page number
110
- - params: {{}}
111
-
112
- Return ONLY valid JSON in this EXACT format:
113
- {{
114
- "pipeline_name": "descriptive-name",
115
- "components": [
116
- {{
117
- "tool_name": "extract_text",
118
- "start_page": 1,
119
- "end_page": 5,
120
- "params": {{"encoding": "utf-8"}}
121
- }},
122
- {{
123
- "tool_name": "summarize_text",
124
- "start_page": 1,
125
- "end_page": 1,
126
- "params": {{"max_length": 500}}
127
- }}
128
- ],
129
- "target_lang": null,
130
- "reason": "Brief explanation of why this pipeline",
131
- "metadata": {{
132
- "estimated_duration_seconds": 30
133
- }}
134
- }}
135
-
136
- IMPORTANT:
137
- - For text processing tools (summarize, classify, NER, translate): start_page=1, end_page=1
138
- - For document extraction tools: use actual page ranges from user request
139
- - Components execute in ORDER - ensure dependencies are met
140
- - Always include "reason" explaining the pipeline choice"""),
141
- ("human", "User request: {input}\n\nFile: {file_path}")
142
- ])
143
-
144
- chain = prompt | llm
145
- response = chain.invoke({
146
- "input": user_input,
147
- "file_path": file_path or "user uploaded document"
148
- })
149
-
150
- # Parse JSON from response
151
- content = response.content
152
-
153
- # Try direct JSON parse
154
- try:
155
- pipeline = json.loads(content)
156
- except json.JSONDecodeError:
157
- # Extract JSON from markdown code blocks
158
- json_match = re.search(r'```json\s*(\{.*?\})\s*```', content, re.DOTALL)
159
- if json_match:
160
- pipeline = json.loads(json_match.group(1))
161
- else:
162
- # Try to find any JSON object
163
- json_match = re.search(r'\{.*\}', content, re.DOTALL)
164
- if json_match:
165
- pipeline = json.loads(json_match.group(0))
166
- else:
167
- raise ValueError(f"No JSON found in Bedrock response: {content}")
168
-
169
- # Add generator metadata
170
- pipeline["_generator"] = "bedrock"
171
- pipeline["_model"] = os.getenv("BEDROCK_MODEL", "anthropic.claude-3-5-sonnet-20241022-v2:0")
172
-
173
- # Validate with Pydantic
174
- validated = PipelineConfig(**pipeline)
175
-
176
- return validated.model_dump()
177
-
178
- except Exception as e:
179
- raise RuntimeError(f"Bedrock pipeline generation failed: {str(e)}")
180
-
181
-
182
- # ========================
183
- # GEMINI PIPELINE GENERATOR
184
- # ========================
185
-
186
- def generate_pipeline_gemini(user_input: str, file_path: Optional[str] = None) -> Dict[str, Any]:
187
- """
188
- Generate pipeline using Google Gemini (fallback method)
189
- """
190
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
191
- GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash")
192
- GEMINI_ENDPOINT = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent"
193
-
194
- if not GEMINI_API_KEY:
195
- raise RuntimeError("Gemini API key not configured")
196
-
197
- prompt = f"""You are a document processing pipeline expert. Generate a detailed pipeline plan.
198
-
199
- Available tools and their parameters:
200
- - extract_text: start_page, end_page, params
201
- - extract_tables: start_page, end_page, params
202
- - describe_images: start_page, end_page, params
203
- - summarize_text: params (no page range)
204
- - classify_text: params (no page range)
205
- - extract_entities: params (no page range)
206
- - translate_text: params with target_lang (no page range)
207
- - signature_verification: start_page, end_page
208
- - stamp_detection: start_page, end_page
209
-
210
- User request: {user_input}
211
- File: {file_path or "user uploaded document"}
212
-
213
- Return ONLY valid JSON in this format:
214
- {{
215
- "pipeline_name": "descriptive-name",
216
- "components": [
217
- {{
218
- "tool_name": "extract_text",
219
- "start_page": 1,
220
- "end_page": 5,
221
- "params": {{}}
222
- }}
223
- ],
224
- "target_lang": null,
225
- "reason": "explanation",
226
- "metadata": {{"estimated_duration_seconds": 30}}
227
- }}"""
228
-
229
- try:
230
- response = requests.post(
231
- f"{GEMINI_ENDPOINT}?key={GEMINI_API_KEY}",
232
- headers={"Content-Type": "application/json"},
233
- json={
234
- "contents": [{"parts": [{"text": prompt}]}],
235
- "generationConfig": {
236
- "temperature": 0.0,
237
- "maxOutputTokens": 1024,
238
- }
239
- },
240
- timeout=60,
241
- )
242
-
243
- response.raise_for_status()
244
- result = response.json()
245
-
246
- # Extract text from Gemini response
247
- content = result["candidates"][0]["content"]["parts"][0]["text"]
248
-
249
- # Parse JSON
250
- try:
251
- pipeline = json.loads(content)
252
- except json.JSONDecodeError:
253
- # Extract from code blocks
254
- json_match = re.search(r'```json\s*(\{.*?\})\s*```', content, re.DOTALL)
255
- if json_match:
256
- pipeline = json.loads(json_match.group(1))
257
- else:
258
- json_match = re.search(r'\{.*\}', content, re.DOTALL)
259
- pipeline = json.loads(json_match.group(0))
260
-
261
- # Add generator metadata
262
- pipeline["_generator"] = "gemini"
263
- pipeline["_model"] = GEMINI_MODEL
264
-
265
- # Validate with Pydantic
266
- validated = PipelineConfig(**pipeline)
267
-
268
- return validated.model_dump()
269
-
270
- except Exception as e:
271
- raise RuntimeError(f"Gemini pipeline generation failed: {str(e)}")
272
-
273
-
274
- # ========================
275
- # UNIFIED PIPELINE GENERATOR WITH FALLBACK
276
- # ========================
277
-
278
- def generate_pipeline(
279
- user_input: str,
280
- file_path: Optional[str] = None,
281
- prefer_bedrock: bool = True
282
- ) -> Dict[str, Any]:
283
- """
284
- Generate pipeline with fallback mechanism.
285
-
286
- Priority:
287
- 1. Try Bedrock (Claude 3.5 Sonnet) - if available and configured
288
- 2. Fallback to Gemini - if Bedrock fails
289
-
290
- Returns:
291
- Pipeline configuration dict with component-level details
292
- """
293
- errors = []
294
-
295
- # Try Bedrock first (priority)
296
- if prefer_bedrock and BEDROCK_AVAILABLE:
297
- try:
298
- print("πŸ† Attempting pipeline generation with Bedrock...")
299
- pipeline = generate_pipeline_bedrock(user_input, file_path)
300
- print(f"βœ… Bedrock pipeline generated successfully: {pipeline['pipeline_name']}")
301
- return pipeline
302
- except Exception as bedrock_error:
303
- error_msg = f"Bedrock failed: {str(bedrock_error)}"
304
- print(f"❌ {error_msg}")
305
- errors.append(error_msg)
306
- print("πŸ”„ Falling back to Gemini...")
307
-
308
- # Fallback to Gemini
309
- try:
310
- print("πŸ”„ Attempting pipeline generation with Gemini...")
311
- pipeline = generate_pipeline_gemini(user_input, file_path)
312
- print(f"βœ… Gemini pipeline generated successfully: {pipeline['pipeline_name']}")
313
-
314
- # Add fallback metadata
315
- if errors:
316
- if "metadata" not in pipeline:
317
- pipeline["metadata"] = {}
318
- pipeline["metadata"]["fallback_reason"] = errors[0]
319
-
320
- return pipeline
321
- except Exception as gemini_error:
322
- error_msg = f"Gemini failed: {str(gemini_error)}"
323
- print(f"❌ {error_msg}")
324
- errors.append(error_msg)
325
-
326
- # Both failed
327
- raise RuntimeError(
328
- f"Pipeline generation failed with all providers.\n"
329
- f"Errors:\n" + "\n".join(f" - {e}" for e in errors)
330
- )
331
-
332
-
333
- # ========================
334
- # UTILITY FUNCTIONS
335
- # ========================
336
-
337
- def format_pipeline_for_display(pipeline: Dict[str, Any]) -> str:
338
- """
339
- Format pipeline as fancy display string for Gradio
340
- """
341
- generator = pipeline.get("_generator", "unknown")
342
- model = pipeline.get("_model", "unknown")
343
-
344
- display = f"""
345
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
346
- 🎯 PIPELINE GENERATED SUCCESSFULLY!
347
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
348
-
349
- πŸ“‹ Pipeline Name: {pipeline.get('pipeline_name', 'unnamed')}
350
- πŸ€– Generated By: {generator.title()} ({model})
351
- ⏱️ Estimated Duration: {pipeline.get('metadata', {}).get('estimated_duration_seconds', 'unknown')} seconds
352
-
353
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
354
- """
355
-
356
- # Add each component
357
- for idx, component in enumerate(pipeline.get("components", []), 1):
358
- tool_name = component.get("tool_name", "unknown")
359
- start_page = component.get("start_page", 1)
360
- end_page = component.get("end_page", 1)
361
- params = component.get("params", {})
362
-
363
- # Icon based on tool type
364
- icon = {
365
- "extract_text": "πŸ“„",
366
- "extract_tables": "πŸ“Š",
367
- "describe_images": "πŸ–ΌοΈ",
368
- "summarize_text": "πŸ“",
369
- "classify_text": "🏷️",
370
- "extract_entities": "πŸ‘€",
371
- "translate_text": "🌐",
372
- "signature_verification": "✍️",
373
- "stamp_detection": "πŸ”–"
374
- }.get(tool_name, "πŸ”§")
375
-
376
- display += f"\n{icon} **STEP {idx}: {tool_name.replace('_', ' ').upper()}**\n"
377
-
378
- if start_page > 1 or end_page > 1:
379
- display += f" πŸ“ Pages: {start_page} to {end_page}\n"
380
-
381
- if params:
382
- display += " βš™οΈ Parameters:\n"
383
- for key, value in params.items():
384
- display += f" β€’ {key}: {value}\n"
385
-
386
- display += "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
387
-
388
- # Add reasoning
389
- display += f"\nπŸ’‘ **REASONING:**\n {pipeline.get('reason', 'No reason provided')}\n"
390
-
391
- display += "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
392
- display += "\nβœ… Type 'approve' to execute this pipeline"
393
- display += "\n❌ Type 'reject' to cancel"
394
- display += "\n✏️ Type 'edit' to modify\n"
395
- display += "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
396
-
397
- return display
398
-
399
-
400
- if __name__ == "__main__":
401
- # Test
402
- test_input = "extract text from pages 1-5, get tables from pages 2-4, and summarize everything"
403
-
404
- try:
405
- pipeline = generate_pipeline(test_input)
406
- print(json.dumps(pipeline, indent=2))
407
- print("\n" + "="*80 + "\n")
408
- print(format_pipeline_for_display(pipeline))
409
- except Exception as e:
410
- print(f"Error: {e}")
 
1
+ # services/pipeline_generator.py
2
+ """
3
+ Unified pipeline generator with Bedrock (priority) and Gemini (fallback)
4
+ """
5
+ import json
6
+ import os
7
+ import re
8
+ from typing import Dict, Any, List, Optional
9
+ from pydantic import BaseModel, Field
10
+
11
+ # For Bedrock
12
+ try:
13
+ from langchain_aws import ChatBedrock
14
+ from langchain_core.prompts import ChatPromptTemplate
15
+ BEDROCK_AVAILABLE = True
16
+ except ImportError:
17
+ BEDROCK_AVAILABLE = False
18
+ print("Warning: langchain_aws not available, Bedrock will be disabled")
19
+
20
+ # For Gemini
21
+ import requests
22
+
23
+
24
+ # ========================
25
+ # PYDANTIC MODELS
26
+ # ========================
27
+
28
+ class ComponentConfig(BaseModel):
29
+ """Configuration for a single pipeline component"""
30
+ tool_name: str = Field(description="Name of the tool to execute")
31
+ start_page: int = Field(default=1, description="Starting page number (1-indexed)")
32
+ end_page: int = Field(default=1, description="Ending page number (inclusive)")
33
+ params: Dict[str, Any] = Field(default_factory=dict, description="Additional tool-specific parameters")
34
+
35
+ class PipelineConfig(BaseModel):
36
+ """Complete pipeline configuration"""
37
+ pipeline_name: str = Field(description="Name/identifier for the pipeline")
38
+ components: List[ComponentConfig] = Field(description="Ordered list of components to execute")
39
+ target_lang: Optional[str] = Field(default=None, description="Target language for translation (if applicable)")
40
+ reason: str = Field(description="AI's reasoning for this pipeline structure")
41
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
42
+
43
+
44
+ # ========================
45
+ # BEDROCK PIPELINE GENERATOR
46
+ # ========================
47
+
48
+ def generate_pipeline_bedrock(user_input: str, file_path: Optional[str] = None) -> Dict[str, Any]:
49
+ """
50
+ Generate pipeline using AWS Bedrock (Mistral Large)
51
+ Priority method - tries this first
52
+ """
53
+ if not BEDROCK_AVAILABLE:
54
+ raise RuntimeError("Bedrock not available - langchain_aws not installed")
55
+
56
+ # Check for AWS credentials
57
+ if not os.getenv("AWS_ACCESS_KEY_ID") or not os.getenv("AWS_SECRET_ACCESS_KEY"):
58
+ raise RuntimeError("AWS credentials not configured")
59
+
60
+ try:
61
+ llm = ChatBedrock(
62
+ model_id="mistral.mistral-large-2402-v1:0",
63
+ region_name=os.getenv("AWS_REGION", "us-east-1"),
64
+ temperature=0.0,
65
+ )
66
+
67
+ prompt = ChatPromptTemplate.from_messages([
68
+ ("system", """You are a document processing pipeline expert. Generate a detailed pipeline plan.
69
+
70
+ Available tools and their parameters:
71
+ 1. extract_text - Extract text from documents
72
+ - start_page (int): Starting page number
73
+ - end_page (int): Ending page number
74
+ - params: {{"encoding": "utf-8", "preserve_layout": bool}}
75
+
76
+ 2. extract_tables - Extract tables from documents
77
+ - start_page (int): Starting page number
78
+ - end_page (int): Ending page number
79
+ - params: {{"format": "json"|"csv", "include_headers": bool}}
80
+
81
+ 3. describe_images - Generate image descriptions
82
+ - start_page (int): Starting page number
83
+ - end_page (int): Ending page number
84
+ - params: {{"detail_level": "low"|"medium"|"high"}}
85
+
86
+ 4. summarize_text - Summarize extracted text
87
+ - No page range (works on extracted text)
88
+ - params: {{"max_length": int, "style": "concise"|"detailed"}}
89
+
90
+ 5. classify_text - Classify document content
91
+ - No page range (works on extracted text)
92
+ - params: {{"categories": list[str]}}
93
+
94
+ 6. extract_entities - Named Entity Recognition
95
+ - No page range (works on extracted text)
96
+ - params: {{"entity_types": list[str]}}
97
+
98
+ 7. translate_text - Translate text to target language
99
+ - No page range (works on extracted text)
100
+ - params: {{"target_lang": str, "source_lang": str}}
101
+
102
+ 8. signature_verification - Verify signatures
103
+ - start_page (int): Starting page number
104
+ - end_page (int): Ending page number
105
+ - params: {{}}
106
+
107
+ 9. stamp_detection - Detect stamps
108
+ - start_page (int): Starting page number
109
+ - end_page (int): Ending page number
110
+ - params: {{}}
111
+
112
+ Return ONLY valid JSON in this EXACT format:
113
+ {{
114
+ "pipeline_name": "descriptive-name",
115
+ "components": [
116
+ {{
117
+ "tool_name": "extract_text",
118
+ "start_page": 1,
119
+ "end_page": 5,
120
+ "params": {{"encoding": "utf-8"}}
121
+ }},
122
+ {{
123
+ "tool_name": "summarize_text",
124
+ "start_page": 1,
125
+ "end_page": 1,
126
+ "params": {{"max_length": 500}}
127
+ }}
128
+ ],
129
+ "target_lang": null,
130
+ "reason": "Brief explanation of why this pipeline",
131
+ "metadata": {{
132
+ "estimated_duration_seconds": 30
133
+ }}
134
+ }}
135
+
136
+ IMPORTANT:
137
+ - For text processing tools (summarize, classify, NER, translate): start_page=1, end_page=1
138
+ - For document extraction tools: use actual page ranges from user request
139
+ - Components execute in ORDER - ensure dependencies are met
140
+ - Always include "reason" explaining the pipeline choice"""),
141
+ ("human", "User request: {input}\n\nFile: {file_path}")
142
+ ])
143
+
144
+ chain = prompt | llm
145
+ response = chain.invoke({
146
+ "input": user_input,
147
+ "file_path": file_path or "user uploaded document"
148
+ })
149
+
150
+ # Parse JSON from response
151
+ content = response.content
152
+
153
+ # Try direct JSON parse
154
+ try:
155
+ pipeline = json.loads(content)
156
+ except json.JSONDecodeError:
157
+ # Extract JSON from markdown code blocks
158
+ json_match = re.search(r'```json\s*(\{.*?\})\s*```', content, re.DOTALL)
159
+ if json_match:
160
+ pipeline = json.loads(json_match.group(1))
161
+ else:
162
+ # Try to find any JSON object
163
+ json_match = re.search(r'\{.*\}', content, re.DOTALL)
164
+ if json_match:
165
+ pipeline = json.loads(json_match.group(0))
166
+ else:
167
+ raise ValueError(f"No JSON found in Bedrock response: {content}")
168
+
169
+ # Add generator metadata
170
+ pipeline["_generator"] = "bedrock"
171
+ pipeline["_model"] = "mistral.mistral-large-2402-v1:0"
172
+
173
+ # Validate with Pydantic
174
+ validated = PipelineConfig(**pipeline)
175
+
176
+ return validated.model_dump()
177
+
178
+ except Exception as e:
179
+ raise RuntimeError(f"Bedrock pipeline generation failed: {str(e)}")
180
+
181
+
182
+ # ========================
183
+ # GEMINI PIPELINE GENERATOR
184
+ # ========================
185
+
186
+ def generate_pipeline_gemini(user_input: str, file_path: Optional[str] = None) -> Dict[str, Any]:
187
+ """
188
+ Generate pipeline using Google Gemini (fallback method)
189
+ """
190
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
191
+ GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.0-flash")
192
+ GEMINI_ENDPOINT = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent"
193
+
194
+ if not GEMINI_API_KEY:
195
+ raise RuntimeError("Gemini API key not configured")
196
+
197
+ prompt = f"""You are a document processing pipeline expert. Generate a detailed pipeline plan.
198
+
199
+ Available tools and their parameters:
200
+ - extract_text: start_page, end_page, params
201
+ - extract_tables: start_page, end_page, params
202
+ - describe_images: start_page, end_page, params
203
+ - summarize_text: params (no page range)
204
+ - classify_text: params (no page range)
205
+ - extract_entities: params (no page range)
206
+ - translate_text: params with target_lang (no page range)
207
+ - signature_verification: start_page, end_page
208
+ - stamp_detection: start_page, end_page
209
+
210
+ User request: {user_input}
211
+ File: {file_path or "user uploaded document"}
212
+
213
+ Return ONLY valid JSON in this format:
214
+ {{
215
+ "pipeline_name": "descriptive-name",
216
+ "components": [
217
+ {{
218
+ "tool_name": "extract_text",
219
+ "start_page": 1,
220
+ "end_page": 5,
221
+ "params": {{}}
222
+ }}
223
+ ],
224
+ "target_lang": null,
225
+ "reason": "explanation",
226
+ "metadata": {{"estimated_duration_seconds": 30}}
227
+ }}"""
228
+
229
+ try:
230
+ response = requests.post(
231
+ f"{GEMINI_ENDPOINT}?key={GEMINI_API_KEY}",
232
+ headers={"Content-Type": "application/json"},
233
+ json={
234
+ "contents": [{"parts": [{"text": prompt}]}],
235
+ "generationConfig": {
236
+ "temperature": 0.0,
237
+ "maxOutputTokens": 1024,
238
+ }
239
+ },
240
+ timeout=60,
241
+ )
242
+
243
+ response.raise_for_status()
244
+ result = response.json()
245
+
246
+ # Extract text from Gemini response
247
+ content = result["candidates"][0]["content"]["parts"][0]["text"]
248
+
249
+ # Parse JSON
250
+ try:
251
+ pipeline = json.loads(content)
252
+ except json.JSONDecodeError:
253
+ # Extract from code blocks
254
+ json_match = re.search(r'```json\s*(\{.*?\})\s*```', content, re.DOTALL)
255
+ if json_match:
256
+ pipeline = json.loads(json_match.group(1))
257
+ else:
258
+ json_match = re.search(r'\{.*\}', content, re.DOTALL)
259
+ pipeline = json.loads(json_match.group(0))
260
+
261
+ # Add generator metadata
262
+ pipeline["_generator"] = "gemini"
263
+ pipeline["_model"] = GEMINI_MODEL
264
+
265
+ # Validate with Pydantic
266
+ validated = PipelineConfig(**pipeline)
267
+
268
+ return validated.model_dump()
269
+
270
+ except Exception as e:
271
+ raise RuntimeError(f"Gemini pipeline generation failed: {str(e)}")
272
+
273
+
274
+ # ========================
275
+ # UNIFIED PIPELINE GENERATOR WITH FALLBACK
276
+ # ========================
277
+
278
+ def generate_pipeline(
279
+ user_input: str,
280
+ file_path: Optional[str] = None,
281
+ prefer_bedrock: bool = True
282
+ ) -> Dict[str, Any]:
283
+ """
284
+ Generate pipeline with fallback mechanism.
285
+
286
+ Priority:
287
+ 1. Try Bedrock (Mistral Large) - if available and configured
288
+ 2. Fallback to Gemini - if Bedrock fails
289
+
290
+ Returns:
291
+ Pipeline configuration dict with component-level details
292
+ """
293
+ errors = []
294
+
295
+ # Try Bedrock first (priority)
296
+ if prefer_bedrock and BEDROCK_AVAILABLE:
297
+ try:
298
+ print("πŸ† Attempting pipeline generation with Bedrock...")
299
+ pipeline = generate_pipeline_bedrock(user_input, file_path)
300
+ print(f"βœ… Bedrock pipeline generated successfully: {pipeline['pipeline_name']}")
301
+ return pipeline
302
+ except Exception as bedrock_error:
303
+ error_msg = f"Bedrock failed: {str(bedrock_error)}"
304
+ print(f"❌ {error_msg}")
305
+ errors.append(error_msg)
306
+ print("πŸ”„ Falling back to Gemini...")
307
+
308
+ # Fallback to Gemini
309
+ try:
310
+ print("πŸ”„ Attempting pipeline generation with Gemini...")
311
+ pipeline = generate_pipeline_gemini(user_input, file_path)
312
+ print(f"βœ… Gemini pipeline generated successfully: {pipeline['pipeline_name']}")
313
+
314
+ # Add fallback metadata
315
+ if errors:
316
+ if "metadata" not in pipeline:
317
+ pipeline["metadata"] = {}
318
+ pipeline["metadata"]["fallback_reason"] = errors[0]
319
+
320
+ return pipeline
321
+ except Exception as gemini_error:
322
+ error_msg = f"Gemini failed: {str(gemini_error)}"
323
+ print(f"❌ {error_msg}")
324
+ errors.append(error_msg)
325
+
326
+ # Both failed
327
+ raise RuntimeError(
328
+ f"Pipeline generation failed with all providers.\n"
329
+ f"Errors:\n" + "\n".join(f" - {e}" for e in errors)
330
+ )
331
+
332
+
333
+ # ========================
334
+ # UTILITY FUNCTIONS
335
+ # ========================
336
+
337
+ def format_pipeline_for_display(pipeline: Dict[str, Any]) -> str:
338
+ """
339
+ Format pipeline as fancy display string for Gradio
340
+ """
341
+ generator = pipeline.get("_generator", "unknown")
342
+ model = pipeline.get("_model", "unknown")
343
+
344
+ display = f"""
345
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
346
+ 🎯 PIPELINE GENERATED SUCCESSFULLY!
347
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
348
+
349
+ πŸ“‹ Pipeline Name: {pipeline.get('pipeline_name', 'unnamed')}
350
+ πŸ€– Generated By: {generator.title()} ({model})
351
+ ⏱️ Estimated Duration: {pipeline.get('metadata', {}).get('estimated_duration_seconds', 'unknown')} seconds
352
+
353
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
354
+ """
355
+
356
+ # Add each component
357
+ for idx, component in enumerate(pipeline.get("components", []), 1):
358
+ tool_name = component.get("tool_name", "unknown")
359
+ start_page = component.get("start_page", 1)
360
+ end_page = component.get("end_page", 1)
361
+ params = component.get("params", {})
362
+
363
+ # Icon based on tool type
364
+ icon = {
365
+ "extract_text": "πŸ“„",
366
+ "extract_tables": "πŸ“Š",
367
+ "describe_images": "πŸ–ΌοΈ",
368
+ "summarize_text": "πŸ“",
369
+ "classify_text": "🏷️",
370
+ "extract_entities": "πŸ‘€",
371
+ "translate_text": "🌐",
372
+ "signature_verification": "✍️",
373
+ "stamp_detection": "πŸ”–"
374
+ }.get(tool_name, "πŸ”§")
375
+
376
+ display += f"\n{icon} **STEP {idx}: {tool_name.replace('_', ' ').upper()}**\n"
377
+
378
+ if start_page > 1 or end_page > 1:
379
+ display += f" πŸ“ Pages: {start_page} to {end_page}\n"
380
+
381
+ if params:
382
+ display += " βš™οΈ Parameters:\n"
383
+ for key, value in params.items():
384
+ display += f" β€’ {key}: {value}\n"
385
+
386
+ display += "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
387
+
388
+ # Add reasoning
389
+ display += f"\nπŸ’‘ **REASONING:**\n {pipeline.get('reason', 'No reason provided')}\n"
390
+
391
+ display += "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
392
+ display += "\nβœ… Type 'approve' to execute this pipeline"
393
+ display += "\n❌ Type 'reject' to cancel"
394
+ display += "\n✏️ Type 'edit' to modify\n"
395
+ display += "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
396
+
397
+ return display
398
+
399
+
400
+ if __name__ == "__main__":
401
+ # Test
402
+ test_input = "extract text from pages 1-5, get tables from pages 2-4, and summarize everything"
403
+
404
+ try:
405
+ pipeline = generate_pipeline(test_input)
406
+ print(json.dumps(pipeline, indent=2))
407
+ print("\n" + "="*80 + "\n")
408
+ print(format_pipeline_for_display(pipeline))
409
+ except Exception as e:
410
+ print(f"Error: {e}")