davidtran999 commited on
Commit
765d69d
·
verified ·
1 Parent(s): 8604302

Upload backend/core/rag.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. backend/core/rag.py +561 -0
backend/core/rag.py ADDED
@@ -0,0 +1,561 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG (Retrieval-Augmented Generation) pipeline for answer generation.
3
+ """
4
+ import re
5
+ import unicodedata
6
+ from typing import List, Dict, Any, Optional
7
+ from .hybrid_search import hybrid_search
8
+ from .models import Procedure, Fine, Office, Advisory, LegalSection
9
+ from hue_portal.chatbot.chatbot import format_fine_amount
10
+ from hue_portal.chatbot.llm_integration import get_llm_generator
11
+ from hue_portal.chatbot.structured_legal import format_structured_legal_answer
12
+
13
+
14
+ def retrieve_top_k_documents(
15
+ query: str,
16
+ content_type: str,
17
+ top_k: int = 5
18
+ ) -> List[Any]:
19
+ """
20
+ Retrieve top-k documents using hybrid search.
21
+
22
+ Args:
23
+ query: Search query.
24
+ content_type: Type of content ('procedure', 'fine', 'office', 'advisory').
25
+ top_k: Number of documents to retrieve.
26
+
27
+ Returns:
28
+ List of document objects.
29
+ """
30
+ # Get appropriate queryset
31
+ if content_type == 'procedure':
32
+ queryset = Procedure.objects.all()
33
+ text_fields = ['title', 'domain', 'conditions', 'dossier']
34
+ elif content_type == 'fine':
35
+ queryset = Fine.objects.all()
36
+ text_fields = ['name', 'code', 'article', 'decree', 'remedial']
37
+ elif content_type == 'office':
38
+ queryset = Office.objects.all()
39
+ text_fields = ['unit_name', 'address', 'district', 'service_scope']
40
+ elif content_type == 'advisory':
41
+ queryset = Advisory.objects.all()
42
+ text_fields = ['title', 'summary']
43
+ elif content_type == 'legal':
44
+ queryset = LegalSection.objects.select_related("document").all()
45
+ text_fields = ['section_title', 'section_code', 'content']
46
+ else:
47
+ return []
48
+
49
+ # Use hybrid search with text_fields for exact match boost
50
+ try:
51
+ from .config.hybrid_search_config import get_config
52
+ config = get_config(content_type)
53
+ results = hybrid_search(
54
+ queryset,
55
+ query,
56
+ top_k=top_k,
57
+ bm25_weight=config.bm25_weight,
58
+ vector_weight=config.vector_weight,
59
+ min_hybrid_score=config.min_hybrid_score,
60
+ text_fields=text_fields
61
+ )
62
+ return results
63
+ except Exception as e:
64
+ print(f"Error in retrieval: {e}")
65
+ return []
66
+
67
+
68
+ def generate_answer_template(
69
+ query: str,
70
+ documents: List[Any],
71
+ content_type: str,
72
+ context: Optional[List[Dict[str, Any]]] = None,
73
+ use_llm: bool = True
74
+ ) -> str:
75
+ """
76
+ Generate answer using LLM (if available) or template-based summarization.
77
+
78
+ Args:
79
+ query: Original query.
80
+ documents: Retrieved documents.
81
+ content_type: Type of content.
82
+ context: Optional conversation context.
83
+ use_llm: Whether to try LLM generation first.
84
+
85
+ Returns:
86
+ Generated answer text.
87
+ """
88
+ def _invoke_llm(documents_for_prompt: List[Any]) -> Optional[str]:
89
+ """Call configured LLM provider safely."""
90
+ try:
91
+ import traceback
92
+ from hue_portal.chatbot.llm_integration import get_llm_generator
93
+
94
+ llm = get_llm_generator()
95
+ if not llm:
96
+ print("[RAG] ⚠️ LLM not available, using template", flush=True)
97
+ return None
98
+
99
+ print(f"[RAG] Using LLM provider: {llm.provider}", flush=True)
100
+ llm_answer = llm.generate_answer(
101
+ query,
102
+ context=context,
103
+ documents=documents_for_prompt
104
+ )
105
+ if llm_answer:
106
+ print(f"[RAG] ✅ LLM answer generated (length: {len(llm_answer)})", flush=True)
107
+ return llm_answer
108
+
109
+ print("[RAG] ⚠️ LLM returned None, using template", flush=True)
110
+ except Exception as exc:
111
+ import traceback
112
+
113
+ error_trace = traceback.format_exc()
114
+ print(f"[RAG] ❌ LLM generation failed, using template: {exc}", flush=True)
115
+ print(f"[RAG] ❌ Trace: {error_trace}", flush=True)
116
+ return None
117
+
118
+ llm_enabled = use_llm or content_type == 'general'
119
+ if llm_enabled:
120
+ llm_documents = documents if documents else []
121
+ llm_answer = _invoke_llm(llm_documents)
122
+ if llm_answer:
123
+ return llm_answer
124
+
125
+ # If no documents, fall back gracefully
126
+ if not documents:
127
+ if content_type == 'general':
128
+ return (
129
+ f"Tôi chưa có dữ liệu pháp luật liên quan đến '{query}', "
130
+ "nhưng vẫn sẵn sàng trò chuyện hoặc hỗ trợ bạn ở chủ đề khác. "
131
+ "Bạn có thể mô tả cụ thể hơn để tôi giúp tốt hơn nhé!"
132
+ )
133
+ return (
134
+ f"Xin lỗi, tôi không tìm thấy thông tin liên quan đến '{query}' trong cơ sở dữ liệu. "
135
+ "Vui lòng thử lại với từ khóa khác hoặc liên hệ trực tiếp với Công an thành phố Huế để được tư vấn."
136
+ )
137
+
138
+ # Fallback to template-based generation
139
+ if content_type == 'procedure':
140
+ return _generate_procedure_answer(query, documents)
141
+ elif content_type == 'fine':
142
+ return _generate_fine_answer(query, documents)
143
+ elif content_type == 'office':
144
+ return _generate_office_answer(query, documents)
145
+ elif content_type == 'advisory':
146
+ return _generate_advisory_answer(query, documents)
147
+ elif content_type == 'legal':
148
+ return _generate_legal_answer(query, documents)
149
+ else:
150
+ return _generate_general_answer(query, documents)
151
+
152
+
153
+ def _generate_procedure_answer(query: str, documents: List[Procedure]) -> str:
154
+ """Generate answer for procedure queries."""
155
+ count = len(documents)
156
+ answer = f"Tôi tìm thấy {count} thủ tục liên quan đến '{query}':\n\n"
157
+
158
+ for i, doc in enumerate(documents[:5], 1):
159
+ answer += f"{i}. {doc.title}\n"
160
+ if doc.domain:
161
+ answer += f" Lĩnh vực: {doc.domain}\n"
162
+ if doc.level:
163
+ answer += f" Cấp: {doc.level}\n"
164
+ if doc.conditions:
165
+ conditions_short = doc.conditions[:100] + "..." if len(doc.conditions) > 100 else doc.conditions
166
+ answer += f" Điều kiện: {conditions_short}\n"
167
+ answer += "\n"
168
+
169
+ if count > 5:
170
+ answer += f"... và {count - 5} thủ tục khác.\n"
171
+
172
+ return answer
173
+
174
+
175
+ def _generate_fine_answer(query: str, documents: List[Fine]) -> str:
176
+ """Generate answer for fine queries."""
177
+ count = len(documents)
178
+ answer = f"Tôi tìm thấy {count} mức phạt liên quan đến '{query}':\n\n"
179
+
180
+ # Highlight best match (first result) if available
181
+ if documents:
182
+ best_match = documents[0]
183
+ answer += "Kết quả chính xác nhất:\n"
184
+ answer += f"• {best_match.name}\n"
185
+ if best_match.code:
186
+ answer += f" Mã vi phạm: {best_match.code}\n"
187
+
188
+ # Format fine amount using helper function
189
+ fine_amount = format_fine_amount(
190
+ float(best_match.min_fine) if best_match.min_fine else None,
191
+ float(best_match.max_fine) if best_match.max_fine else None
192
+ )
193
+ if fine_amount:
194
+ answer += f" Mức phạt: {fine_amount}\n"
195
+
196
+ if best_match.article:
197
+ answer += f" Điều luật: {best_match.article}\n"
198
+ answer += "\n"
199
+
200
+ # Add other results if available
201
+ if count > 1:
202
+ answer += "Các mức phạt khác:\n"
203
+ for i, doc in enumerate(documents[1:5], 2):
204
+ answer += f"{i}. {doc.name}\n"
205
+ if doc.code:
206
+ answer += f" Mã vi phạm: {doc.code}\n"
207
+
208
+ # Format fine amount
209
+ fine_amount = format_fine_amount(
210
+ float(doc.min_fine) if doc.min_fine else None,
211
+ float(doc.max_fine) if doc.max_fine else None
212
+ )
213
+ if fine_amount:
214
+ answer += f" Mức phạt: {fine_amount}\n"
215
+
216
+ if doc.article:
217
+ answer += f" Điều luật: {doc.article}\n"
218
+ answer += "\n"
219
+ else:
220
+ # Fallback if no documents
221
+ for i, doc in enumerate(documents[:5], 1):
222
+ answer += f"{i}. {doc.name}\n"
223
+ if doc.code:
224
+ answer += f" Mã vi phạm: {doc.code}\n"
225
+
226
+ # Format fine amount
227
+ fine_amount = format_fine_amount(
228
+ float(doc.min_fine) if doc.min_fine else None,
229
+ float(doc.max_fine) if doc.max_fine else None
230
+ )
231
+ if fine_amount:
232
+ answer += f" Mức phạt: {fine_amount}\n"
233
+
234
+ if doc.article:
235
+ answer += f" Điều luật: {doc.article}\n"
236
+ answer += "\n"
237
+
238
+ if count > 5:
239
+ answer += f"... và {count - 5} mức phạt khác.\n"
240
+
241
+ return answer
242
+
243
+
244
+ def _generate_office_answer(query: str, documents: List[Office]) -> str:
245
+ """Generate answer for office queries."""
246
+ count = len(documents)
247
+ answer = f"Tôi tìm thấy {count} đơn vị liên quan đến '{query}':\n\n"
248
+
249
+ for i, doc in enumerate(documents[:5], 1):
250
+ answer += f"{i}. {doc.unit_name}\n"
251
+ if doc.address:
252
+ answer += f" Địa chỉ: {doc.address}\n"
253
+ if doc.district:
254
+ answer += f" Quận/Huyện: {doc.district}\n"
255
+ if doc.phone:
256
+ answer += f" Điện thoại: {doc.phone}\n"
257
+ if doc.working_hours:
258
+ answer += f" Giờ làm việc: {doc.working_hours}\n"
259
+ answer += "\n"
260
+
261
+ if count > 5:
262
+ answer += f"... và {count - 5} đơn vị khác.\n"
263
+
264
+ return answer
265
+
266
+
267
+ def _generate_advisory_answer(query: str, documents: List[Advisory]) -> str:
268
+ """Generate answer for advisory queries."""
269
+ count = len(documents)
270
+ answer = f"Tôi tìm thấy {count} cảnh báo liên quan đến '{query}':\n\n"
271
+
272
+ for i, doc in enumerate(documents[:5], 1):
273
+ answer += f"{i}. {doc.title}\n"
274
+ if doc.summary:
275
+ summary_short = doc.summary[:150] + "..." if len(doc.summary) > 150 else doc.summary
276
+ answer += f" {summary_short}\n"
277
+ answer += "\n"
278
+
279
+ if count > 5:
280
+ answer += f"... và {count - 5} cảnh báo khác.\n"
281
+
282
+ return answer
283
+
284
+
285
+ def _clean_text(value: str) -> str:
286
+ """Normalize whitespace and strip noise for legal snippets."""
287
+ if not value:
288
+ return ""
289
+ compressed = re.sub(r"\s+", " ", value)
290
+ return compressed.strip()
291
+
292
+
293
+ def _summarize_section(
294
+ section: LegalSection,
295
+ max_sentences: int = 3,
296
+ max_chars: int = 600
297
+ ) -> str:
298
+ """
299
+ Produce a concise Vietnamese summary directly from the stored content.
300
+
301
+ This is used as the Vietnamese prefill before calling the LLM so we avoid
302
+ English drift and keep the answer grounded.
303
+ """
304
+ content = _clean_text(section.content)
305
+ if not content:
306
+ return ""
307
+
308
+ # Split by sentence boundaries; fall back to chunks if delimiters missing.
309
+ sentences = re.split(r"(?<=[.!?])\s+", content)
310
+ if not sentences:
311
+ sentences = [content]
312
+
313
+ summary_parts = []
314
+ for sentence in sentences:
315
+ if not sentence:
316
+ continue
317
+ summary_parts.append(sentence)
318
+ joined = " ".join(summary_parts)
319
+ if len(summary_parts) >= max_sentences or len(joined) >= max_chars:
320
+ break
321
+
322
+ summary = " ".join(summary_parts)
323
+ if len(summary) > max_chars:
324
+ summary = summary[:max_chars].rsplit(" ", 1)[0] + "..."
325
+ return summary.strip()
326
+
327
+
328
+ def _format_citation(section: LegalSection) -> str:
329
+ citation = section.document.title
330
+ if section.section_code:
331
+ citation = f"{citation} – {section.section_code}"
332
+ page = ""
333
+ if section.page_start:
334
+ page = f" (trang {section.page_start}"
335
+ if section.page_end and section.page_end != section.page_start:
336
+ page += f"-{section.page_end}"
337
+ page += ")"
338
+ return f"{citation}{page}".strip()
339
+
340
+
341
+ def _build_legal_prefill(documents: List[LegalSection]) -> str:
342
+ """
343
+ Build a compact Vietnamese summary block that will be injected into the
344
+ Guardrails prompt. The goal is to bias the model toward Vietnamese output.
345
+ """
346
+ if not documents:
347
+ return ""
348
+
349
+ lines = ["Bản tóm tắt tiếng Việt từ cơ sở dữ liệu:"]
350
+ for idx, section in enumerate(documents[:3], start=1):
351
+ summary = _summarize_section(section, max_sentences=2, max_chars=400)
352
+ citation = _format_citation(section)
353
+ if not summary:
354
+ continue
355
+ lines.append(f"{idx}. {summary} (Nguồn: {citation})")
356
+
357
+ return "\n".join(lines)
358
+
359
+
360
+ def _generate_legal_citation_block(documents: List[LegalSection]) -> str:
361
+ """Return formatted citation block reused by multiple answer modes."""
362
+ if not documents:
363
+ return ""
364
+
365
+ lines: List[str] = []
366
+ for idx, section in enumerate(documents[:5], start=1):
367
+ summary = _summarize_section(section)
368
+ snippet = _clean_text(section.content)[:350]
369
+ if snippet and len(snippet) == 350:
370
+ snippet = snippet.rsplit(" ", 1)[0] + "..."
371
+ citation = _format_citation(section)
372
+
373
+ lines.append(f"{idx}. {section.section_title or 'Nội dung'} – {citation}")
374
+ if summary:
375
+ lines.append(f" - Tóm tắt: {summary}")
376
+ if snippet:
377
+ lines.append(f" - Trích dẫn: \"{snippet}\"")
378
+ lines.append("")
379
+
380
+ if len(documents) > 5:
381
+ lines.append(f"... và {len(documents) - 5} trích đoạn khác trong cùng nguồn dữ liệu.")
382
+
383
+ return "\n".join(lines).strip()
384
+
385
+
386
+ def _generate_legal_answer(query: str, documents: List[LegalSection]) -> str:
387
+ count = len(documents)
388
+ if count == 0:
389
+ return (
390
+ f"Tôi chưa tìm thấy trích dẫn pháp lý nào cho '{query}'. "
391
+ "Bạn có thể cung cấp thêm ngữ cảnh để tôi tiếp tục hỗ trợ."
392
+ )
393
+
394
+ header = (
395
+ f"Tôi đã tổng hợp {count} trích đoạn pháp lý liên quan đến '{query}'. "
396
+ "Đây là bản tóm tắt tiếng Việt kèm trích dẫn:"
397
+ )
398
+ citation_block = _generate_legal_citation_block(documents)
399
+ return f"{header}\n\n{citation_block}".strip()
400
+
401
+
402
+ def _generate_general_answer(query: str, documents: List[Any]) -> str:
403
+ """Generate general answer."""
404
+ count = len(documents)
405
+ return f"Tôi tìm thấy {count} kết quả liên quan đến '{query}'. Vui lòng xem chi tiết bên dưới."
406
+
407
+
408
+ def _strip_accents(value: str) -> str:
409
+ return "".join(
410
+ char for char in unicodedata.normalize("NFD", value)
411
+ if unicodedata.category(char) != "Mn"
412
+ )
413
+
414
+
415
+ def _contains_markers(
416
+ text_with_accents: str,
417
+ text_without_accents: str,
418
+ markers: List[str]
419
+ ) -> bool:
420
+ for marker in markers:
421
+ marker_lower = marker.lower()
422
+ marker_no_accents = _strip_accents(marker_lower)
423
+ if marker_lower in text_with_accents or marker_no_accents in text_without_accents:
424
+ return True
425
+ return False
426
+
427
+
428
+ def _is_valid_legal_answer(answer: str, documents: List[LegalSection]) -> bool:
429
+ """
430
+ Validate that the LLM answer for legal intent references actual legal content.
431
+
432
+ Criteria:
433
+ - Must not contain denial phrases (already handled earlier) or "xin lỗi".
434
+ - Must not introduce obvious monetary values (legal documents không có số tiền phạt).
435
+ - Must have tối thiểu 40 ký tự để tránh câu trả lời quá ngắn.
436
+ """
437
+ if not answer:
438
+ return False
439
+
440
+ normalized_answer = answer.lower()
441
+ normalized_answer_no_accents = _strip_accents(normalized_answer)
442
+
443
+ denial_markers = [
444
+ "xin lỗi",
445
+ "thông tin trong cơ sở dữ liệu chưa đủ",
446
+ "không thể giúp",
447
+ "không tìm thấy thông tin",
448
+ "không có dữ liệu",
449
+ ]
450
+ if _contains_markers(normalized_answer, normalized_answer_no_accents, denial_markers):
451
+ return False
452
+
453
+ money_markers = ["vnđ", "vnd", "đồng", "đ", "dong"]
454
+ if _contains_markers(normalized_answer, normalized_answer_no_accents, money_markers):
455
+ return False
456
+
457
+ if len(answer.strip()) < 40:
458
+ return False
459
+
460
+ return True
461
+
462
+
463
+ def rag_pipeline(
464
+ query: str,
465
+ intent: str,
466
+ top_k: int = 5,
467
+ min_confidence: float = 0.3,
468
+ context: Optional[List[Dict[str, Any]]] = None,
469
+ use_llm: bool = True
470
+ ) -> Dict[str, Any]:
471
+ """
472
+ Complete RAG pipeline: retrieval + answer generation.
473
+
474
+ Args:
475
+ query: User query.
476
+ intent: Detected intent.
477
+ top_k: Number of documents to retrieve.
478
+ min_confidence: Minimum confidence threshold.
479
+ context: Optional conversation context.
480
+ use_llm: Whether to use LLM for answer generation.
481
+
482
+ Returns:
483
+ Dictionary with 'answer', 'documents', 'count', 'confidence', 'content_type'.
484
+ """
485
+ # Map intent to content type
486
+ intent_to_type = {
487
+ 'search_procedure': 'procedure',
488
+ 'search_fine': 'fine',
489
+ 'search_office': 'office',
490
+ 'search_advisory': 'advisory',
491
+ 'search_legal': 'legal',
492
+ 'general_query': 'general',
493
+ 'greeting': 'general',
494
+ }
495
+
496
+ content_type = intent_to_type.get(intent, 'procedure')
497
+
498
+ # Retrieve documents
499
+ documents = retrieve_top_k_documents(query, content_type, top_k=top_k)
500
+
501
+ # Enable LLM automatically for casual conversation intents
502
+ llm_allowed = use_llm or intent in {"general_query", "greeting"}
503
+
504
+ structured_used = False
505
+ answer: Optional[str] = None
506
+
507
+ if intent == "search_legal" and documents:
508
+ llm = get_llm_generator()
509
+ if llm:
510
+ prefill_summary = _build_legal_prefill(documents)
511
+ structured = llm.generate_structured_legal_answer(
512
+ query,
513
+ documents,
514
+ prefill_summary=prefill_summary,
515
+ )
516
+ if structured:
517
+ answer = format_structured_legal_answer(structured)
518
+ structured_used = True
519
+ citation_block = _generate_legal_citation_block(documents)
520
+ if citation_block:
521
+ answer = (
522
+ f"{answer.rstrip()}\n\nTrích dẫn chi tiết:\n{citation_block}"
523
+ )
524
+
525
+ if answer is None:
526
+ answer = generate_answer_template(
527
+ query,
528
+ documents,
529
+ content_type,
530
+ context=context,
531
+ use_llm=llm_allowed
532
+ )
533
+
534
+ # Fallback nếu intent pháp luật nhưng câu LLM không đạt tiêu chí
535
+ if (
536
+ intent == "search_legal"
537
+ and documents
538
+ and isinstance(answer, str)
539
+ and not structured_used
540
+ ):
541
+ if not _is_valid_legal_answer(answer, documents):
542
+ print("[RAG] ⚠️ Fallback: invalid legal answer detected", flush=True)
543
+ answer = _generate_legal_answer(query, documents)
544
+ else:
545
+ citation_block = _generate_legal_answer(query, documents)
546
+ if citation_block.strip():
547
+ answer = f"{answer.rstrip()}\n\nTrích dẫn chi tiết:\n{citation_block}"
548
+
549
+ # Calculate confidence (simple: based on number of results and scores)
550
+ confidence = min(1.0, len(documents) / top_k)
551
+ if documents and hasattr(documents[0], '_hybrid_score'):
552
+ confidence = max(confidence, documents[0]._hybrid_score)
553
+
554
+ return {
555
+ 'answer': answer,
556
+ 'documents': documents,
557
+ 'count': len(documents),
558
+ 'confidence': confidence,
559
+ 'content_type': content_type
560
+ }
561
+