44New high-performance analysis endpoints that replace the slow multi-agent system.
55"""
66from flask import Blueprint , request , jsonify , g
7+ import threading
8+ import time
79
810from app .utils .auth import login_required
911from app .utils .logger import get_logger
1012from app .services .fast_analysis import get_fast_analysis_service
1113from app .services .analysis_memory import get_analysis_memory
14+ from app .services .billing_service import get_billing_service
1215
1316logger = get_logger (__name__ )
1417
1518fast_analysis_bp = Blueprint ('fast_analysis' , __name__ )
1619
20+ # In-memory in-flight guard to avoid duplicate analysis charges caused by rapid repeated clicks.
21+ _analysis_inflight_lock = threading .Lock ()
22+ _analysis_inflight = {} # key -> expire_ts
23+
24+
25+ def _build_inflight_key (user_id : int , market : str , symbol : str , timeframe : str ) -> str :
26+ return f"{ int (user_id )} |{ str (market or '' ).strip ().upper ()} |{ str (symbol or '' ).strip ().upper ()} |{ str (timeframe or '' ).strip ().upper ()} "
27+
28+
29+ def _acquire_inflight (key : str , ttl_sec : int = 90 ) -> bool :
30+ now = time .time ()
31+ with _analysis_inflight_lock :
32+ # Cleanup stale entries
33+ stale = [k for k , exp in _analysis_inflight .items () if float (exp ) <= now ]
34+ for k in stale [:1024 ]:
35+ _analysis_inflight .pop (k , None )
36+ if key in _analysis_inflight and float (_analysis_inflight .get (key ) or 0 ) > now :
37+ return False
38+ _analysis_inflight [key ] = now + int (ttl_sec )
39+ return True
40+
41+
42+ def _release_inflight (key : str ):
43+ with _analysis_inflight_lock :
44+ _analysis_inflight .pop (key , None )
45+
1746
1847@fast_analysis_bp .route ('/analyze' , methods = ['POST' ])
1948@login_required
@@ -51,6 +80,58 @@ def analyze():
5180
5281 # Get current user's ID to associate analysis with user
5382 user_id = getattr (g , 'user_id' , None )
83+ if not user_id :
84+ return jsonify ({'code' : 0 , 'msg' : 'Unauthorized' , 'data' : None }), 401
85+
86+ inflight_key = _build_inflight_key (user_id , market , symbol , timeframe )
87+ if not _acquire_inflight (inflight_key , ttl_sec = 90 ):
88+ return jsonify ({
89+ 'code' : 0 ,
90+ 'msg' : 'Analysis already in progress for this symbol/timeframe. Please wait.' ,
91+ 'data' : {'in_progress' : True }
92+ }), 429
93+
94+ # Billing / credits (best-effort, consistent with polymarket deep analysis)
95+ credits_charged = 0
96+ remaining_credits = None
97+ billing_consumed = False
98+ billing = None
99+ try :
100+ billing = get_billing_service ()
101+ if billing .is_billing_enabled ():
102+ credits_charged = int (billing .get_feature_cost ('ai_analysis' ) or 0 )
103+ if credits_charged > 0 :
104+ ok , msg = billing .check_and_consume (
105+ user_id = int (user_id ),
106+ feature = 'ai_analysis' ,
107+ reference_id = f"fast_analysis_{ market } :{ symbol } :{ timeframe } "
108+ )
109+ if not ok :
110+ # Standardize insufficient credits message
111+ if str (msg or "" ).startswith ('insufficient_credits' ):
112+ # Format: insufficient_credits:<current>:<cost>
113+ parts = str (msg ).split (':' )
114+ cur = float (parts [1 ]) if len (parts ) >= 2 else 0.0
115+ req = float (parts [2 ]) if len (parts ) >= 3 else float (credits_charged )
116+ return jsonify ({
117+ 'code' : 0 ,
118+ 'msg' : 'Insufficient credits' ,
119+ 'data' : {
120+ 'required' : req ,
121+ 'current' : cur ,
122+ 'shortage' : max (0.0 , req - cur ),
123+ }
124+ }), 400
125+ return jsonify ({'code' : 0 , 'msg' : f'Failed to deduct credits: { msg } ' , 'data' : None }), 500
126+ billing_consumed = True
127+ # Query remaining credits after successful consumption
128+ try :
129+ remaining_credits = float (billing .get_user_credits (int (user_id )))
130+ except Exception :
131+ remaining_credits = None
132+ except Exception as e :
133+ # Billing failure should not crash analysis by default, but should be visible in logs.
134+ logger .warning (f"Billing check failed (skipped): { e } " , exc_info = True )
54135
55136 service = get_fast_analysis_service ()
56137 result = service .analyze (
@@ -63,6 +144,18 @@ def analyze():
63144 )
64145
65146 if result .get ('error' ):
147+ # Best-effort refund if we already charged but analysis failed.
148+ if billing_consumed and billing and credits_charged > 0 :
149+ try :
150+ billing .add_credits (
151+ user_id = int (user_id ),
152+ amount = int (credits_charged ),
153+ action = 'refund' ,
154+ remark = f'Auto refund: fast-analysis failed ({ market } :{ symbol } :{ timeframe } )'
155+ )
156+ remaining_credits = float (billing .get_user_credits (int (user_id )))
157+ except Exception as re :
158+ logger .error (f"Auto refund failed: { re } " , exc_info = True )
66159 return jsonify ({
67160 'code' : 0 ,
68161 'msg' : result ['error' ],
@@ -75,16 +168,37 @@ def analyze():
75168 return jsonify ({
76169 'code' : 1 ,
77170 'msg' : 'success' ,
78- 'data' : result
171+ 'data' : {
172+ ** (result or {}),
173+ 'credits_charged' : credits_charged ,
174+ 'remaining_credits' : remaining_credits ,
175+ }
79176 })
80177
81178 except Exception as e :
179+ # Best-effort refund on unexpected error after charge.
180+ try :
181+ if 'billing_consumed' in locals () and billing_consumed and 'billing' in locals () and billing and credits_charged > 0 and 'user_id' in locals () and user_id :
182+ billing .add_credits (
183+ user_id = int (user_id ),
184+ amount = int (credits_charged ),
185+ action = 'refund' ,
186+ remark = f'Auto refund: fast-analysis exception ({ market } :{ symbol } :{ timeframe } )'
187+ )
188+ except Exception :
189+ pass
82190 logger .error (f"Fast analysis API failed: { e } " , exc_info = True )
83191 return jsonify ({
84192 'code' : 0 ,
85193 'msg' : str (e ),
86194 'data' : None
87195 }), 500
196+ finally :
197+ try :
198+ if 'inflight_key' in locals () and inflight_key :
199+ _release_inflight (inflight_key )
200+ except Exception :
201+ pass
88202
89203
90204@fast_analysis_bp .route ('/analyze-legacy' , methods = ['POST' ])
@@ -115,6 +229,56 @@ def analyze_legacy():
115229 'msg' : 'market and symbol are required' ,
116230 'data' : None
117231 }), 400
232+
233+ # Billing / credits (same behavior as /analyze)
234+ user_id = getattr (g , 'user_id' , None )
235+ if not user_id :
236+ return jsonify ({'code' : 0 , 'msg' : 'Unauthorized' , 'data' : None }), 401
237+
238+ inflight_key = _build_inflight_key (user_id , market , symbol , timeframe )
239+ if not _acquire_inflight (inflight_key , ttl_sec = 90 ):
240+ return jsonify ({
241+ 'code' : 0 ,
242+ 'msg' : 'Analysis already in progress for this symbol/timeframe. Please wait.' ,
243+ 'data' : {'in_progress' : True }
244+ }), 429
245+
246+ credits_charged = 0
247+ remaining_credits = None
248+ billing_consumed = False
249+ billing = None
250+ try :
251+ billing = get_billing_service ()
252+ if billing .is_billing_enabled ():
253+ credits_charged = int (billing .get_feature_cost ('ai_analysis' ) or 0 )
254+ if credits_charged > 0 :
255+ ok , msg = billing .check_and_consume (
256+ user_id = int (user_id ),
257+ feature = 'ai_analysis' ,
258+ reference_id = f"fast_analysis_legacy_{ market } :{ symbol } :{ timeframe } "
259+ )
260+ if not ok :
261+ if str (msg or "" ).startswith ('insufficient_credits' ):
262+ parts = str (msg ).split (':' )
263+ cur = float (parts [1 ]) if len (parts ) >= 2 else 0.0
264+ req = float (parts [2 ]) if len (parts ) >= 3 else float (credits_charged )
265+ return jsonify ({
266+ 'code' : 0 ,
267+ 'msg' : 'Insufficient credits' ,
268+ 'data' : {
269+ 'required' : req ,
270+ 'current' : cur ,
271+ 'shortage' : max (0.0 , req - cur ),
272+ }
273+ }), 400
274+ return jsonify ({'code' : 0 , 'msg' : f'Failed to deduct credits: { msg } ' , 'data' : None }), 500
275+ billing_consumed = True
276+ try :
277+ remaining_credits = float (billing .get_user_credits (int (user_id )))
278+ except Exception :
279+ remaining_credits = None
280+ except Exception as e :
281+ logger .warning (f"Billing check failed (skipped): { e } " , exc_info = True )
118282
119283 service = get_fast_analysis_service ()
120284 result = service .analyze_legacy_format (
@@ -126,6 +290,17 @@ def analyze_legacy():
126290 )
127291
128292 if result .get ('error' ):
293+ if billing_consumed and billing and credits_charged > 0 :
294+ try :
295+ billing .add_credits (
296+ user_id = int (user_id ),
297+ amount = int (credits_charged ),
298+ action = 'refund' ,
299+ remark = f'Auto refund: fast-analysis-legacy failed ({ market } :{ symbol } :{ timeframe } )'
300+ )
301+ remaining_credits = float (billing .get_user_credits (int (user_id )))
302+ except Exception as re :
303+ logger .error (f"Auto refund failed (legacy): { re } " , exc_info = True )
129304 return jsonify ({
130305 'code' : 0 ,
131306 'msg' : result ['error' ],
@@ -135,16 +310,36 @@ def analyze_legacy():
135310 return jsonify ({
136311 'code' : 1 ,
137312 'msg' : 'success' ,
138- 'data' : result
313+ 'data' : {
314+ ** (result or {}),
315+ 'credits_charged' : credits_charged ,
316+ 'remaining_credits' : remaining_credits ,
317+ }
139318 })
140319
141320 except Exception as e :
321+ try :
322+ if 'billing_consumed' in locals () and billing_consumed and 'billing' in locals () and billing and credits_charged > 0 and 'user_id' in locals () and user_id :
323+ billing .add_credits (
324+ user_id = int (user_id ),
325+ amount = int (credits_charged ),
326+ action = 'refund' ,
327+ remark = f'Auto refund: fast-analysis-legacy exception ({ market } :{ symbol } :{ timeframe } )'
328+ )
329+ except Exception :
330+ pass
142331 logger .error (f"Fast analysis legacy API failed: { e } " , exc_info = True )
143332 return jsonify ({
144333 'code' : 0 ,
145334 'msg' : str (e ),
146335 'data' : None
147336 }), 500
337+ finally :
338+ try :
339+ if 'inflight_key' in locals () and inflight_key :
340+ _release_inflight (inflight_key )
341+ except Exception :
342+ pass
148343
149344
150345@fast_analysis_bp .route ('/history' , methods = ['GET' ])
0 commit comments