ming commited on
Commit
b47201f
·
1 Parent(s): 4c4036e

Fix Outlines API usage for V4 JSON streaming endpoint

Browse files

- Update to use outlines.generate.json() API pattern
- Fix 'takes 1 positional argument but 2 were given' error
- Add improved test script with detailed error reporting

app/services/structured_summarizer.py CHANGED
@@ -54,7 +54,7 @@ from pydantic import BaseModel
54
  # Try to import Outlines for JSON schema enforcement
55
  OUTLINES_AVAILABLE = False
56
  outlines_models = None
57
- outlines_generate_json = None
58
 
59
  try:
60
  import outlines
@@ -69,55 +69,16 @@ try:
69
  logger.warning("Could not import outlines.models")
70
  raise
71
 
72
- # Try different import patterns for JSON generation
73
- # Based on the attributes, we see 'generator' and 'json_schema'
74
- outlines_generate_json = None
75
-
76
- # Pattern 1: outlines.generator.json (most likely based on 'generator' attribute)
77
  try:
78
- import outlines.generator as gen_module
79
- if hasattr(gen_module, 'json'):
80
- outlines_generate_json = gen_module.json
81
- logger.info("Found outlines.generator.json via generator module")
82
- except (ImportError, AttributeError) as e:
83
- logger.debug(f"Pattern 1 failed: {e}")
84
-
85
- # Pattern 2: from outlines.generator import json
86
- if outlines_generate_json is None:
87
- try:
88
- from outlines.generator import json as outlines_generate_json
89
- logger.info("Found outlines.generator.json via submodule import")
90
- except ImportError as e:
91
- logger.debug(f"Pattern 2 failed: {e}")
92
-
93
- # Pattern 3: Check if generator is a module on outlines
94
- if outlines_generate_json is None:
95
- if hasattr(outlines, 'generator'):
96
- gen_attr = getattr(outlines, 'generator')
97
- if hasattr(gen_attr, 'json'):
98
- outlines_generate_json = gen_attr.json
99
- logger.info("Found generator.json as attribute")
100
-
101
- # Pattern 4: Maybe it's outlines.json_schema or similar
102
- if outlines_generate_json is None:
103
- if hasattr(outlines, 'json_schema'):
104
- js_attr = getattr(outlines, 'json_schema')
105
- if callable(js_attr):
106
- outlines_generate_json = js_attr
107
- logger.info("Found json_schema as callable")
108
 
109
- # Pattern 5: Check if Generator class has a json method
110
- if outlines_generate_json is None:
111
- if hasattr(outlines, 'Generator'):
112
- gen_class = getattr(outlines, 'Generator')
113
- if hasattr(gen_class, 'json') or hasattr(gen_class, 'from_json'):
114
- # Generator class might have a different API
115
- logger.info("Found Generator class, checking for json method")
116
- # We'll need to use it differently - this might require model wrapping
117
- # For now, let's check if there's a simpler way
118
-
119
- if outlines_generate_json is None:
120
- raise ImportError(f"Could not find generator.json. Available in outlines: {available_attrs[:10]}...")
121
 
122
  OUTLINES_AVAILABLE = True
123
  logger.info("✅ Outlines library imported successfully")
@@ -290,26 +251,16 @@ class StructuredSummarizer:
290
  logger.error(f"❌ V4 model warmup failed: {e}")
291
 
292
  # Also warm up Outlines JSON generation
293
- if OUTLINES_AVAILABLE and self.outlines_model is not None:
294
  try:
295
- # Try the same pattern as in the streaming method
296
- try:
297
- json_gen_func = outlines_generate_json(StructuredSummary)
298
- dummy_gen = json_gen_func(self.outlines_model)
299
- except TypeError:
300
- dummy_gen = outlines_generate_json(self.outlines_model, StructuredSummary)
301
 
302
- # Try to call it
303
- try:
304
- result = dummy_gen("Warmup text for Outlines structured summary.")
305
- # Consume the generator if it's a generator
306
- if hasattr(result, '__iter__'):
307
- _ = list(result)[:1] # Just consume first item for warmup
308
- except TypeError:
309
- # Maybe it needs prompt as keyword arg or different pattern
310
- result = dummy_gen.stream("Warmup text") if hasattr(dummy_gen, 'stream') else dummy_gen("Warmup")
311
- if hasattr(result, '__iter__'):
312
- _ = list(result)[:1]
313
 
314
  logger.info("✅ V4 Outlines JSON warmup successful")
315
  except Exception as e:
@@ -1011,36 +962,22 @@ Rules:
1011
 
1012
  try:
1013
  # Check if Outlines is available
1014
- if not OUTLINES_AVAILABLE:
1015
  error_obj = {"error": "Outlines library not available. Please install outlines>=0.0.34."}
1016
  yield json.dumps(error_obj)
1017
  return
1018
 
1019
- # Create an Outlines generator bound to the StructuredSummary schema
1020
- # In version 0.0.34, the API might be different - try different patterns
1021
- try:
1022
- # Pattern 1: json_schema(schema) returns a function that takes model and prompt
1023
- json_generator_func = outlines_generate_json(StructuredSummary)
1024
- json_generator = json_generator_func(self.outlines_model)
1025
- except TypeError:
1026
- # Pattern 2: json_schema(model, schema) - original pattern
1027
- json_generator = outlines_generate_json(self.outlines_model, StructuredSummary)
1028
-
1029
  start_time = time.time()
1030
 
1031
- # Stream tokens; each token is a string fragment of the final JSON object
1032
- # The generator might have .stream() method or be directly iterable
1033
- try:
1034
- # Try .stream() method first
1035
- token_iter = json_generator.stream(prompt)
1036
- except AttributeError:
1037
- # If no .stream(), try calling it directly with prompt
1038
- try:
1039
- token_iter = json_generator(prompt)
1040
- except TypeError:
1041
- # Last resort: maybe it's a generator factory that needs model and prompt
1042
- token_iter = json_generator(self.outlines_model, prompt)
1043
 
 
1044
  for token in token_iter:
1045
  # Each `token` is a raw string fragment; just pass it through
1046
  if token:
 
54
  # Try to import Outlines for JSON schema enforcement
55
  OUTLINES_AVAILABLE = False
56
  outlines_models = None
57
+ outlines_generate = None
58
 
59
  try:
60
  import outlines
 
69
  logger.warning("Could not import outlines.models")
70
  raise
71
 
72
+ # Try to import generate module (for outlines.generate.json)
 
 
 
 
73
  try:
74
+ from outlines import generate as outlines_generate
75
+ logger.info("✅ Found outlines.generate module")
76
+ except ImportError as e:
77
+ logger.warning(f"Could not import outlines.generate: {e}")
78
+ outlines_generate = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
+ if outlines_generate is None:
81
+ raise ImportError(f"Could not import outlines.generate. Available in outlines: {available_attrs[:10]}...")
 
 
 
 
 
 
 
 
 
 
82
 
83
  OUTLINES_AVAILABLE = True
84
  logger.info("✅ Outlines library imported successfully")
 
251
  logger.error(f"❌ V4 model warmup failed: {e}")
252
 
253
  # Also warm up Outlines JSON generation
254
+ if OUTLINES_AVAILABLE and self.outlines_model is not None and outlines_generate is not None:
255
  try:
256
+ # Use outlines.generate.json(model, schema) pattern
257
+ json_generator = outlines_generate.json(self.outlines_model, StructuredSummary)
 
 
 
 
258
 
259
+ # Try to call it with a simple prompt
260
+ result = json_generator("Warmup text for Outlines structured summary.")
261
+ # Consume the generator if it's a generator
262
+ if hasattr(result, '__iter__') and not isinstance(result, str):
263
+ _ = list(result)[:1] # Just consume first item for warmup
 
 
 
 
 
 
264
 
265
  logger.info("✅ V4 Outlines JSON warmup successful")
266
  except Exception as e:
 
962
 
963
  try:
964
  # Check if Outlines is available
965
+ if not OUTLINES_AVAILABLE or outlines_generate is None:
966
  error_obj = {"error": "Outlines library not available. Please install outlines>=0.0.34."}
967
  yield json.dumps(error_obj)
968
  return
969
 
 
 
 
 
 
 
 
 
 
 
970
  start_time = time.time()
971
 
972
+ # Create an Outlines generator bound to the StructuredSummary schema
973
+ # Modern Outlines API: outlines.generate.json(model, schema)
974
+ json_generator = outlines_generate.json(self.outlines_model, StructuredSummary)
975
+
976
+ # Call the generator with the prompt to get streaming tokens
977
+ # The generator returns an iterable of string tokens
978
+ token_iter = json_generator(prompt)
 
 
 
 
 
979
 
980
+ # Stream tokens; each token is a string fragment of the final JSON object
981
  for token in token_iter:
982
  # Each `token` is a raw string fragment; just pass it through
983
  if token:
test_hf_v4_stream_json.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test the Hugging Face V4 JSON streaming endpoint with Outlines.
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+
8
+ import httpx
9
+
10
+
11
+ async def test_hf_stream_json_endpoint():
12
+ """Test HF V4 JSON streaming endpoint with URL scraping."""
13
+
14
+ # Hugging Face Space URL
15
+ hf_space_url = "https://colin730-summarizerapp.hf.space"
16
+
17
+ url = "https://www.nzherald.co.nz/nz/auckland/mt-wellington-homicide-jury-find-couple-not-guilty-of-murder-after-soldier-stormed-their-house-with-knife/B56S6KBHRVFCZMLDI56AZES6KY/"
18
+
19
+ print("=" * 80)
20
+ print("Hugging Face V4 JSON Streaming Endpoint Test (Outlines)")
21
+ print("=" * 80)
22
+ print(f"\nHF Space: {hf_space_url}")
23
+ print(f"Endpoint: {hf_space_url}/api/v4/scrape-and-summarize/stream-json")
24
+ print(f"Article URL: {url[:80]}...")
25
+ print(f"Style: executive\n")
26
+
27
+ payload = {
28
+ "url": url,
29
+ "style": "executive",
30
+ "include_metadata": True,
31
+ "use_cache": True,
32
+ }
33
+
34
+ # Longer timeout for HF (first request can be slow if cold start)
35
+ async with httpx.AsyncClient(timeout=600.0) as client:
36
+ try:
37
+ print("🔄 Sending request to Hugging Face...")
38
+ print("⏱️ Note: First request may take 30-60s if instance is cold\n")
39
+
40
+ # Make streaming request
41
+ async with client.stream(
42
+ "POST",
43
+ f"{hf_space_url}/api/v4/scrape-and-summarize/stream-json",
44
+ json=payload,
45
+ ) as response:
46
+ print(f"Status: {response.status_code}")
47
+
48
+ if response.status_code != 200:
49
+ error_text = await response.aread()
50
+ error_str = error_text.decode()
51
+ print(f"\n❌ Error Response:")
52
+ print(error_str)
53
+
54
+ # Check if it's a 404 (endpoint not found)
55
+ if response.status_code == 404:
56
+ print("\n💡 The endpoint might not be deployed yet.")
57
+ print(" The HF Space may still be building (~5-10 minutes).")
58
+ print(f" Check status at: https://huggingface.co/spaces/colin730/SummarizerApp")
59
+ return
60
+
61
+ print("\n" + "=" * 80)
62
+ print("STREAMING JSON TOKENS")
63
+ print("=" * 80)
64
+
65
+ metadata = None
66
+ json_buffer = ""
67
+ token_count = 0
68
+
69
+ # Parse SSE stream
70
+ async for line in response.aiter_lines():
71
+ if line.startswith("data: "):
72
+ data_content = line[6:] # Remove "data: " prefix
73
+
74
+ try:
75
+ # Try to parse as JSON (might be metadata or error event)
76
+ try:
77
+ event = json.loads(data_content)
78
+
79
+ # Handle metadata event
80
+ if event.get("type") == "metadata":
81
+ metadata = event["data"]
82
+ print("\n--- Metadata Event ---")
83
+ print(json.dumps(metadata, indent=2))
84
+ print("\n" + "-" * 80)
85
+ continue
86
+
87
+ # Handle error event
88
+ if event.get("type") == "error" or "error" in event:
89
+ error_msg = event.get('error', 'Unknown error')
90
+ error_detail = event.get('detail', '')
91
+ print(f"\n❌ ERROR: {error_msg}")
92
+ if error_detail:
93
+ print(f" Detail: {error_detail}")
94
+ if "Outlines" in str(event.get("error", "")) or "Outlines" in str(error_detail):
95
+ print("\n💡 This means:")
96
+ print(" - The endpoint is working ✅")
97
+ print(" - But Outlines is not available/installed")
98
+ print(f"\nFull error event:")
99
+ print(json.dumps(event, indent=2))
100
+ return
101
+
102
+ except json.JSONDecodeError:
103
+ # This is a raw JSON token - concatenate it
104
+ json_buffer += data_content
105
+ token_count += 1
106
+ if token_count % 10 == 0:
107
+ print(f"📝 Received {token_count} tokens...", end="\r")
108
+
109
+ except Exception as e:
110
+ print(f"\n⚠️ Error processing line: {e}")
111
+ print(f"Raw: {data_content[:100]}")
112
+
113
+ # Print final results
114
+ print("\n" + "=" * 80)
115
+ print("FINAL RESULTS")
116
+ print("=" * 80)
117
+
118
+ if metadata:
119
+ print(f"\n--- Scraping Info ---")
120
+ print(f"Input type: {metadata.get('input_type')}")
121
+ print(f"Article title: {metadata.get('title')}")
122
+ print(f"Site: {metadata.get('site_name')}")
123
+ print(f"Scrape method: {metadata.get('scrape_method')}")
124
+ print(f"Scrape latency: {metadata.get('scrape_latency_ms', 0):.2f}ms")
125
+ print(f"Text extracted: {metadata.get('extracted_text_length', 0)} chars")
126
+
127
+ print(f"\nTotal tokens received: {token_count}")
128
+ print(f"JSON buffer length: {len(json_buffer)} chars")
129
+
130
+ # Try to parse the complete JSON
131
+ if json_buffer.strip():
132
+ try:
133
+ final_json = json.loads(json_buffer)
134
+
135
+ # Check if the JSON itself is an error object
136
+ if "error" in final_json:
137
+ print(f"\n❌ ERROR IN JSON RESPONSE:")
138
+ print(f" Error: {final_json.get('error', 'Unknown error')}")
139
+ if "detail" in final_json:
140
+ print(f" Detail: {final_json.get('detail', '')}")
141
+ print(f"\nFull error JSON:")
142
+ print(json.dumps(final_json, indent=2))
143
+ return
144
+
145
+ print("\n--- Final JSON Object (StructuredSummary) ---")
146
+ print(json.dumps(final_json, indent=2, ensure_ascii=False))
147
+
148
+ # Validate structure
149
+ print("\n--- Validation ---")
150
+ required_fields = ["title", "main_summary", "key_points", "category", "sentiment", "read_time_min"]
151
+
152
+ all_valid = True
153
+ for field in required_fields:
154
+ value = final_json.get(field)
155
+ if field == "key_points":
156
+ if isinstance(value, list) and len(value) > 0:
157
+ print(f"✅ {field}: {len(value)} items")
158
+ else:
159
+ print(f"⚠️ {field}: empty or not a list")
160
+ all_valid = False
161
+ else:
162
+ if value is not None:
163
+ value_str = str(value)[:50] + "..." if len(str(value)) > 50 else str(value)
164
+ print(f"✅ {field}: {value_str}")
165
+ else:
166
+ print(f"⚠️ {field}: None")
167
+ all_valid = False
168
+
169
+ # Check sentiment is valid
170
+ sentiment = final_json.get("sentiment")
171
+ valid_sentiments = ["positive", "negative", "neutral"]
172
+ if sentiment in valid_sentiments:
173
+ print(f"✅ sentiment value is valid: {sentiment}")
174
+ else:
175
+ print(f"⚠️ sentiment value is invalid: {sentiment}")
176
+ all_valid = False
177
+
178
+ print("\n" + "=" * 80)
179
+ if all_valid:
180
+ print("✅ ALL VALIDATIONS PASSED - HF JSON STREAMING ENDPOINT WORKING!")
181
+ print("✅ Outlines JSON schema enforcement is working!")
182
+ else:
183
+ print("⚠️ Some validations failed")
184
+ print("=" * 80)
185
+
186
+ except json.JSONDecodeError as e:
187
+ print(f"\n❌ Failed to parse final JSON: {e}")
188
+ print(f"\nJSON buffer (first 500 chars):")
189
+ print(json_buffer[:500])
190
+ print("\n💡 The JSON might be incomplete or malformed")
191
+ else:
192
+ print("\n⚠️ No JSON tokens received")
193
+
194
+ except httpx.ConnectError:
195
+ print(f"\n❌ Could not connect to {hf_space_url}")
196
+ print("\n💡 Possible reasons:")
197
+ print(" 1. HF Space is still building/deploying")
198
+ print(" 2. HF Space is sleeping (free tier)")
199
+ print(" 3. Network connectivity issue")
200
+ print(f"\n🔗 Check space status: https://huggingface.co/spaces/colin730/SummarizerApp")
201
+ except httpx.ReadTimeout:
202
+ print("\n⏱️ Request timed out")
203
+ print(" This might mean the HF Space is cold-starting")
204
+ print(" Try again in a few moments")
205
+ except Exception as e:
206
+ print(f"\n❌ Error: {e}")
207
+ import traceback
208
+ traceback.print_exc()
209
+
210
+
211
+ if __name__ == "__main__":
212
+ print("\n🚀 Testing Hugging Face V4 JSON Streaming Endpoint (Outlines)\n")
213
+ asyncio.run(test_hf_stream_json_endpoint())
214
+