ihtesham0345 commited on
Commit
2cf0f7b
·
1 Parent(s): 758a047

fix: Guard against LLM returning JSON array instead of object

Browse files
Files changed (1) hide show
  1. services/utils.py +35 -32
services/utils.py CHANGED
@@ -1,6 +1,7 @@
1
  import json
2
  import re
3
 
 
4
  def clean_json_string(text: str) -> str:
5
  if "```json" in text:
6
  text = text.split("```json")[1].split("```")[0]
@@ -12,22 +13,24 @@ def clean_json_string(text: str) -> str:
12
  text = parts[1]
13
  return text.strip()
14
 
 
15
  def repair_json(json_str: str) -> str:
16
  json_str = json_str.strip()
17
  json_str = json_str.rstrip(", ")
18
-
19
  open_braces = json_str.count("{")
20
  close_braces = json_str.count("}")
21
  open_brackets = json_str.count("[")
22
  close_brackets = json_str.count("]")
23
-
24
  if open_braces > close_braces:
25
  json_str += "}" * (open_braces - close_braces)
26
  if open_brackets > close_brackets:
27
  json_str += "]" * (open_brackets - close_brackets)
28
-
29
  return json_str
30
 
 
31
  def parse_and_repair(raw_text: str, max_preview=300):
32
  cleaned = clean_json_string(raw_text)
33
  try:
@@ -44,16 +47,17 @@ def parse_and_repair(raw_text: str, max_preview=300):
44
  except json.JSONDecodeError as e:
45
  return None, {"error": str(e), "raw_preview": raw_text[:max_preview]}
46
 
 
47
  def repair_json_v2(json_str: str) -> str:
48
  json_str = json_str.strip()
49
  json_str = json_str.rstrip(", ")
50
-
51
  in_string = False
52
  escape = False
53
  brace_depth = 0
54
  bracket_depth = 0
55
  last_good_pos = 0
56
-
57
  for i, ch in enumerate(json_str):
58
  if escape:
59
  escape = False
@@ -66,7 +70,7 @@ def repair_json_v2(json_str: str) -> str:
66
  continue
67
  if in_string:
68
  continue
69
-
70
  if ch == '{':
71
  brace_depth += 1
72
  elif ch == '}':
@@ -75,52 +79,50 @@ def repair_json_v2(json_str: str) -> str:
75
  bracket_depth += 1
76
  elif ch == ']':
77
  bracket_depth -= 1
78
-
79
  if brace_depth >= 0 and bracket_depth >= 0:
80
  last_good_pos = i + 1
81
-
82
  end = last_good_pos
83
  trimmed = json_str[:end].rstrip(", ")
84
-
85
  cb = trimmed.count("{") - trimmed.count("}")
86
  sb = trimmed.count("[") - trimmed.count("]")
87
-
88
  if cb > 0:
89
  trimmed += "}" * cb
90
  if sb > 0:
91
  trimmed += "]" * sb
92
-
93
  return trimmed
94
 
 
95
  def normalize_field(items, expected_type):
96
  """Convert model output to match Pydantic expectations."""
97
  if not isinstance(items, list):
98
  return items
99
 
100
  if expected_type == "object_list":
101
- # Items should be [{key: val, ...}, ...] but model gave ["str1", "str2"]
102
  if items and isinstance(items[0], str):
103
  return [{"title": s, "expected_ctr": "Medium"} for s in items]
104
  return items
105
 
106
  if expected_type == "string_list":
107
- # Items should be ["str1", "str2"] but model gave [{...}, {...}]
108
  if items and isinstance(items[0], dict):
109
  extracted = []
110
  for obj in items:
111
- val = obj.get("caption") or obj.get("description") or obj.get("title") or obj.get("name") or obj.get("url") or str(obj)
 
112
  extracted.append(val)
113
  return extracted
114
  return items
115
 
116
  if expected_type == "keyword_list":
117
- # Items should be [{keyword, search_volume, competition, relevance}]
118
  if items and isinstance(items[0], str):
119
  return [{"keyword": s, "search_volume": "Medium", "competition": "Medium", "relevance": 80} for s in items]
120
  return items
121
 
122
  if expected_type == "hashtag_list":
123
- # Items should be [{tag, post_count}] but model gave ["#tag1", "#tag2"]
124
  if items and isinstance(items[0], str):
125
  return [{"tag": s, "post_count": "N/A"} for s in items]
126
  return items
@@ -137,40 +139,37 @@ def normalize_field(items, expected_type):
137
 
138
  return items
139
 
 
140
  def normalize_response(data: dict) -> dict:
141
  """Post-process model output to match expected Pydantic schemas."""
142
- # YouTube: video_titles expects list of {title, expected_ctr}
143
  if "video_titles" in data:
144
  data["video_titles"] = normalize_field(data["video_titles"], "object_list")
145
  if "thumbnail_ideas" in data:
146
  data["thumbnail_ideas"] = normalize_field(data["thumbnail_ideas"], "string_list")
147
 
148
- # Instagram
149
  if "captions" in data:
150
  data["captions"] = normalize_field(data["captions"], "caption_list")
151
  if "hashtag_sets" in data and isinstance(data["hashtag_sets"], dict):
152
  for tier in ["small", "medium", "large"]:
153
  if tier in data["hashtag_sets"] and isinstance(data["hashtag_sets"][tier], list):
154
  if data["hashtag_sets"][tier] and isinstance(data["hashtag_sets"][tier][0], dict):
155
- data["hashtag_sets"][tier] = [obj.get("tag", obj.get("name", str(obj))) for obj in data["hashtag_sets"][tier]]
 
156
  if "content_ideas" in data and isinstance(data["content_ideas"], dict):
157
  for key in ["reels", "carousels", "stories"]:
158
  if key in data["content_ideas"]:
159
  data["content_ideas"][key] = normalize_field(data["content_ideas"][key], "string_list")
160
 
161
- # Generic SEO
162
  if "core_keywords" in data:
163
  data["core_keywords"] = normalize_field(data["core_keywords"], "keyword_list")
164
  if "viral_hashtags" in data:
165
  data["viral_hashtags"] = normalize_field(data["viral_hashtags"], "hashtag_list")
166
 
167
- # Pinterest
168
  if "pin_ideas" in data:
169
  data["pin_ideas"] = normalize_field(data["pin_ideas"], "pin_list")
170
  if "seo_keywords" in data:
171
  data["seo_keywords"] = normalize_field(data["seo_keywords"], "string_list")
172
 
173
- # Generic string lists that might have gotten objects
174
  for field in ["related_phrases", "strategy_tips", "content_titles",
175
  "tags", "engagement_strategies", "growth_strategies",
176
  "article_topics", "thought_leadership_angles",
@@ -182,31 +181,29 @@ def normalize_response(data: dict) -> dict:
182
  if field in data:
183
  data[field] = normalize_field(data[field], "string_list")
184
 
185
- # LinkedIn post_drafts: should be [{headline, body, hook}]
186
  if "post_drafts" in data:
187
  if data["post_drafts"] and isinstance(data["post_drafts"][0], str):
188
  data["post_drafts"] = [{"headline": s, "body": s, "hook": s} for s in data["post_drafts"]]
189
 
190
- # Post ideas: should be [{type, content}]
191
  if "post_ideas" in data:
192
  if data["post_ideas"] and isinstance(data["post_ideas"][0], str):
193
  data["post_ideas"] = [{"type": "text", "content": s} for s in data["post_ideas"]]
194
 
195
- # Tweet threads: should be [{tweets: [str], theme: str}]
196
  if "tweet_threads" in data:
197
  if data["tweet_threads"] and isinstance(data["tweet_threads"][0], str):
198
  data["tweet_threads"] = [{"tweets": [data["tweet_threads"][0]], "theme": "topic"}]
199
 
200
- # Video concepts: should be [{hook, script_snippet, sound_suggestion}]
201
  if "video_concepts" in data:
202
  if data["video_concepts"] and isinstance(data["video_concepts"][0], str):
203
- data["video_concepts"] = [{"hook": s, "script_snippet": s, "sound_suggestion": "Trending"} for s in data["video_concepts"]]
 
204
 
205
- # Grammar: corrections should be objects [{original, corrected, error_type, explanation}]
206
  if "corrections" in data:
207
  if isinstance(data["corrections"], list) and data["corrections"]:
208
  if isinstance(data["corrections"][0], str):
209
- data["corrections"] = [{"original": s, "corrected": s, "error_type": "grammar", "explanation": "Automatically corrected."} for s in data["corrections"]]
 
 
210
  elif isinstance(data["corrections"][0], dict):
211
  for c in data["corrections"]:
212
  c.setdefault("error_type", "grammar")
@@ -214,7 +211,8 @@ def normalize_response(data: dict) -> dict:
214
  if "issues" in data:
215
  if isinstance(data["issues"], list) and data["issues"]:
216
  if isinstance(data["issues"][0], str):
217
- data["issues"] = [{"issue_type": s, "location": "text", "suggestion": "Review this section."} for s in data["issues"]]
 
218
  elif isinstance(data["issues"][0], dict):
219
  for iss in data["issues"]:
220
  iss.setdefault("issue_type", "style")
@@ -238,22 +236,22 @@ def normalize_response(data: dict) -> dict:
238
  except (ValueError, TypeError):
239
  data["sentence_count"] = 0
240
 
241
- # Ensure descriptions are strings, not objects
242
  if "description_template" in data and isinstance(data["description_template"], dict):
243
  data["description_template"] = str(data["description_template"])
244
 
245
- # Target audience should be string
246
  if "target_audience" in data and isinstance(data["target_audience"], dict):
247
  data["target_audience"] = str(data["target_audience"])
248
 
249
  return data
250
 
 
251
  def validate_and_fill_data_defaults(data: dict, defaults: dict) -> dict:
252
  for key, default_val in defaults.items():
253
  if key not in data or data[key] is None:
254
  data[key] = default_val
255
  return data
256
 
 
257
  def run_analysis(messages, defaults=None, temperature=0.3, max_new_tokens=2000):
258
  try:
259
  from services.model_loader import generate_text
@@ -274,6 +272,11 @@ def run_analysis(messages, defaults=None, temperature=0.3, max_new_tokens=2000):
274
  base["error"] = "The AI response was malformed. Please try again."
275
  return base
276
 
 
 
 
 
 
277
  try:
278
  data = normalize_response(data)
279
  except Exception as e:
 
1
  import json
2
  import re
3
 
4
+
5
  def clean_json_string(text: str) -> str:
6
  if "```json" in text:
7
  text = text.split("```json")[1].split("```")[0]
 
13
  text = parts[1]
14
  return text.strip()
15
 
16
+
17
  def repair_json(json_str: str) -> str:
18
  json_str = json_str.strip()
19
  json_str = json_str.rstrip(", ")
20
+
21
  open_braces = json_str.count("{")
22
  close_braces = json_str.count("}")
23
  open_brackets = json_str.count("[")
24
  close_brackets = json_str.count("]")
25
+
26
  if open_braces > close_braces:
27
  json_str += "}" * (open_braces - close_braces)
28
  if open_brackets > close_brackets:
29
  json_str += "]" * (open_brackets - close_brackets)
30
+
31
  return json_str
32
 
33
+
34
  def parse_and_repair(raw_text: str, max_preview=300):
35
  cleaned = clean_json_string(raw_text)
36
  try:
 
47
  except json.JSONDecodeError as e:
48
  return None, {"error": str(e), "raw_preview": raw_text[:max_preview]}
49
 
50
+
51
  def repair_json_v2(json_str: str) -> str:
52
  json_str = json_str.strip()
53
  json_str = json_str.rstrip(", ")
54
+
55
  in_string = False
56
  escape = False
57
  brace_depth = 0
58
  bracket_depth = 0
59
  last_good_pos = 0
60
+
61
  for i, ch in enumerate(json_str):
62
  if escape:
63
  escape = False
 
70
  continue
71
  if in_string:
72
  continue
73
+
74
  if ch == '{':
75
  brace_depth += 1
76
  elif ch == '}':
 
79
  bracket_depth += 1
80
  elif ch == ']':
81
  bracket_depth -= 1
82
+
83
  if brace_depth >= 0 and bracket_depth >= 0:
84
  last_good_pos = i + 1
85
+
86
  end = last_good_pos
87
  trimmed = json_str[:end].rstrip(", ")
88
+
89
  cb = trimmed.count("{") - trimmed.count("}")
90
  sb = trimmed.count("[") - trimmed.count("]")
91
+
92
  if cb > 0:
93
  trimmed += "}" * cb
94
  if sb > 0:
95
  trimmed += "]" * sb
96
+
97
  return trimmed
98
 
99
+
100
  def normalize_field(items, expected_type):
101
  """Convert model output to match Pydantic expectations."""
102
  if not isinstance(items, list):
103
  return items
104
 
105
  if expected_type == "object_list":
 
106
  if items and isinstance(items[0], str):
107
  return [{"title": s, "expected_ctr": "Medium"} for s in items]
108
  return items
109
 
110
  if expected_type == "string_list":
 
111
  if items and isinstance(items[0], dict):
112
  extracted = []
113
  for obj in items:
114
+ val = obj.get("caption") or obj.get("description") or obj.get("title") or obj.get("name") or obj.get(
115
+ "url") or str(obj)
116
  extracted.append(val)
117
  return extracted
118
  return items
119
 
120
  if expected_type == "keyword_list":
 
121
  if items and isinstance(items[0], str):
122
  return [{"keyword": s, "search_volume": "Medium", "competition": "Medium", "relevance": 80} for s in items]
123
  return items
124
 
125
  if expected_type == "hashtag_list":
 
126
  if items and isinstance(items[0], str):
127
  return [{"tag": s, "post_count": "N/A"} for s in items]
128
  return items
 
139
 
140
  return items
141
 
142
+
143
  def normalize_response(data: dict) -> dict:
144
  """Post-process model output to match expected Pydantic schemas."""
 
145
  if "video_titles" in data:
146
  data["video_titles"] = normalize_field(data["video_titles"], "object_list")
147
  if "thumbnail_ideas" in data:
148
  data["thumbnail_ideas"] = normalize_field(data["thumbnail_ideas"], "string_list")
149
 
 
150
  if "captions" in data:
151
  data["captions"] = normalize_field(data["captions"], "caption_list")
152
  if "hashtag_sets" in data and isinstance(data["hashtag_sets"], dict):
153
  for tier in ["small", "medium", "large"]:
154
  if tier in data["hashtag_sets"] and isinstance(data["hashtag_sets"][tier], list):
155
  if data["hashtag_sets"][tier] and isinstance(data["hashtag_sets"][tier][0], dict):
156
+ data["hashtag_sets"][tier] = [obj.get("tag", obj.get("name", str(obj))) for obj in
157
+ data["hashtag_sets"][tier]]
158
  if "content_ideas" in data and isinstance(data["content_ideas"], dict):
159
  for key in ["reels", "carousels", "stories"]:
160
  if key in data["content_ideas"]:
161
  data["content_ideas"][key] = normalize_field(data["content_ideas"][key], "string_list")
162
 
 
163
  if "core_keywords" in data:
164
  data["core_keywords"] = normalize_field(data["core_keywords"], "keyword_list")
165
  if "viral_hashtags" in data:
166
  data["viral_hashtags"] = normalize_field(data["viral_hashtags"], "hashtag_list")
167
 
 
168
  if "pin_ideas" in data:
169
  data["pin_ideas"] = normalize_field(data["pin_ideas"], "pin_list")
170
  if "seo_keywords" in data:
171
  data["seo_keywords"] = normalize_field(data["seo_keywords"], "string_list")
172
 
 
173
  for field in ["related_phrases", "strategy_tips", "content_titles",
174
  "tags", "engagement_strategies", "growth_strategies",
175
  "article_topics", "thought_leadership_angles",
 
181
  if field in data:
182
  data[field] = normalize_field(data[field], "string_list")
183
 
 
184
  if "post_drafts" in data:
185
  if data["post_drafts"] and isinstance(data["post_drafts"][0], str):
186
  data["post_drafts"] = [{"headline": s, "body": s, "hook": s} for s in data["post_drafts"]]
187
 
 
188
  if "post_ideas" in data:
189
  if data["post_ideas"] and isinstance(data["post_ideas"][0], str):
190
  data["post_ideas"] = [{"type": "text", "content": s} for s in data["post_ideas"]]
191
 
 
192
  if "tweet_threads" in data:
193
  if data["tweet_threads"] and isinstance(data["tweet_threads"][0], str):
194
  data["tweet_threads"] = [{"tweets": [data["tweet_threads"][0]], "theme": "topic"}]
195
 
 
196
  if "video_concepts" in data:
197
  if data["video_concepts"] and isinstance(data["video_concepts"][0], str):
198
+ data["video_concepts"] = [{"hook": s, "script_snippet": s, "sound_suggestion": "Trending"} for s in
199
+ data["video_concepts"]]
200
 
 
201
  if "corrections" in data:
202
  if isinstance(data["corrections"], list) and data["corrections"]:
203
  if isinstance(data["corrections"][0], str):
204
+ data["corrections"] = [
205
+ {"original": s, "corrected": s, "error_type": "grammar", "explanation": "Automatically corrected."}
206
+ for s in data["corrections"]]
207
  elif isinstance(data["corrections"][0], dict):
208
  for c in data["corrections"]:
209
  c.setdefault("error_type", "grammar")
 
211
  if "issues" in data:
212
  if isinstance(data["issues"], list) and data["issues"]:
213
  if isinstance(data["issues"][0], str):
214
+ data["issues"] = [{"issue_type": s, "location": "text", "suggestion": "Review this section."} for s in
215
+ data["issues"]]
216
  elif isinstance(data["issues"][0], dict):
217
  for iss in data["issues"]:
218
  iss.setdefault("issue_type", "style")
 
236
  except (ValueError, TypeError):
237
  data["sentence_count"] = 0
238
 
 
239
  if "description_template" in data and isinstance(data["description_template"], dict):
240
  data["description_template"] = str(data["description_template"])
241
 
 
242
  if "target_audience" in data and isinstance(data["target_audience"], dict):
243
  data["target_audience"] = str(data["target_audience"])
244
 
245
  return data
246
 
247
+
248
  def validate_and_fill_data_defaults(data: dict, defaults: dict) -> dict:
249
  for key, default_val in defaults.items():
250
  if key not in data or data[key] is None:
251
  data[key] = default_val
252
  return data
253
 
254
+
255
  def run_analysis(messages, defaults=None, temperature=0.3, max_new_tokens=2000):
256
  try:
257
  from services.model_loader import generate_text
 
272
  base["error"] = "The AI response was malformed. Please try again."
273
  return base
274
 
275
+ if not isinstance(data, dict):
276
+ print(f"[run_analysis] Expected dict, got {type(data).__name__}")
277
+ base = dict(defaults) if defaults else {}
278
+ return base
279
+
280
  try:
281
  data = normalize_response(data)
282
  except Exception as e: