StavanShah01 commited on
Commit
9fbdcab
·
1 Parent(s): 0ac86f4

feat: mount Flask WSGI + Gradio on unified FastAPI ASGI app

Browse files
Files changed (3) hide show
  1. api/app.py +5 -0
  2. app.py +55 -78
  3. requirements.txt +2 -0
api/app.py CHANGED
@@ -120,11 +120,13 @@ def create_app() -> Flask:
120
  })
121
 
122
  @app.get("/api/search")
 
123
  def search() -> Json:
124
  """Companies matching `q`; a query under three characters matches none."""
125
  return jsonify(find(request.args.get("q", ""))), 200
126
 
127
  @app.get("/api/stocks/<symbol>/report")
 
128
  def report(symbol: str) -> Json:
129
  """The Drive link to this symbol's report."""
130
  sym, refresh = _sym(symbol), _flag("refresh")
@@ -145,6 +147,7 @@ def create_app() -> Flask:
145
  preview_link=up.preview_link)), 200
146
 
147
  @app.get("/api/stocks/<symbol>/report/file")
 
148
  def report_file(symbol: str) -> Response:
149
  """The PDF itself. `?download=1` sends it as an attachment."""
150
  pdf, _ = _pdf(_sym(symbol), _flag("refresh"))
@@ -154,6 +157,7 @@ def create_app() -> Flask:
154
  # --- OpenAI-Compatible Reverse Proxy Endpoints (Client / Frontend Safe) ---
155
  @app.get("/v1/models")
156
  @app.get("/api/llm/v1/models")
 
157
  def list_models() -> Json:
158
  """Lists supported SLM / LLM chat, embed, and rerank models."""
159
  model_names = [m.strip() for m in DEFAULT_CHAT_MODELS.split(",") if m.strip()]
@@ -175,6 +179,7 @@ def create_app() -> Flask:
175
 
176
  @app.post("/v1/chat/completions")
177
  @app.post("/api/llm/v1/chat/completions")
 
178
  def chat_completions() -> Response:
179
  """Secure reverse proxy for OpenAI-compatible chat completions with unbuffered streaming."""
180
  payload = request.get_json(force=True, silent=True) or {}
 
120
  })
121
 
122
  @app.get("/api/search")
123
+ @app.get("/search")
124
  def search() -> Json:
125
  """Companies matching `q`; a query under three characters matches none."""
126
  return jsonify(find(request.args.get("q", ""))), 200
127
 
128
  @app.get("/api/stocks/<symbol>/report")
129
+ @app.get("/stocks/<symbol>/report")
130
  def report(symbol: str) -> Json:
131
  """The Drive link to this symbol's report."""
132
  sym, refresh = _sym(symbol), _flag("refresh")
 
147
  preview_link=up.preview_link)), 200
148
 
149
  @app.get("/api/stocks/<symbol>/report/file")
150
+ @app.get("/stocks/<symbol>/report/file")
151
  def report_file(symbol: str) -> Response:
152
  """The PDF itself. `?download=1` sends it as an attachment."""
153
  pdf, _ = _pdf(_sym(symbol), _flag("refresh"))
 
157
  # --- OpenAI-Compatible Reverse Proxy Endpoints (Client / Frontend Safe) ---
158
  @app.get("/v1/models")
159
  @app.get("/api/llm/v1/models")
160
+ @app.get("/models")
161
  def list_models() -> Json:
162
  """Lists supported SLM / LLM chat, embed, and rerank models."""
163
  model_names = [m.strip() for m in DEFAULT_CHAT_MODELS.split(",") if m.strip()]
 
179
 
180
  @app.post("/v1/chat/completions")
181
  @app.post("/api/llm/v1/chat/completions")
182
+ @app.post("/chat/completions")
183
  def chat_completions() -> Response:
184
  """Secure reverse proxy for OpenAI-compatible chat completions with unbuffered streaming."""
185
  payload = request.get_json(force=True, silent=True) or {}
app.py CHANGED
@@ -1,11 +1,9 @@
1
  """GrowNXT Institutional Equity Research Platform -- Hugging Face Spaces & Local Entrypoint.
2
 
3
- Runs the GrowNXT Flask API backend in a background worker and exposes an interactive
4
- Gradio UI on port 7860 with multi-tab support:
5
- 1. Institutional Report Generator (Typst PDF + Google Drive Delivery)
6
- 2. Listed Stock Search
7
- 3. Real-Time SLM Financial Analyst Chat (Direct Streaming via Qwen / Gemma)
8
- 4. REST API Documentation & Reverse Proxy Status
9
 
10
  Google Python Style Guide Compliant.
11
  """
@@ -15,7 +13,6 @@ from __future__ import annotations
15
  import json
16
  import logging
17
  import os
18
- import threading
19
  import time
20
  from typing import Any, Dict, Generator, List, Optional, Tuple
21
 
@@ -23,8 +20,12 @@ from typing import Any, Dict, Generator, List, Optional, Tuple
23
  os.environ["GRADIO_SSR_MODE"] = "False"
24
 
25
  from dotenv import load_dotenv
 
 
 
26
  import gradio as gr
27
  import requests
 
28
 
29
  # Patch gradio_client bug with Pydantic v2 boolean additionalProperties schemas
30
  try:
@@ -47,6 +48,8 @@ except Exception:
47
  pass
48
 
49
  from api.app import create_app
 
 
50
  from core.llm_config import (
51
  ACTIVE_MODEL,
52
  DEFAULT_BASE_URL,
@@ -55,6 +58,8 @@ from core.llm_config import (
55
  clean_thinking_tokens,
56
  stream_llm_response,
57
  )
 
 
58
 
59
  load_dotenv()
60
 
@@ -64,53 +69,43 @@ logging.basicConfig(
64
  )
65
  logger = logging.getLogger("grownxt.spaces")
66
 
67
- INTERNAL_PORT = int(os.getenv("INTERNAL_PORT", "5000"))
68
  HF_PORT = int(os.getenv("PORT", "7860"))
69
 
70
  # ---------------------------------------------------------------------------
71
- # 1. Background Flask Backend Daemon
72
  # ---------------------------------------------------------------------------
73
  flask_app = create_app()
 
 
 
 
 
 
 
 
 
74
 
75
-
76
- def start_flask_worker() -> None:
77
- """Launches the internal Flask API server in a background thread."""
78
- logger.info("Starting internal GrowNXT Flask API backend on port %d...", INTERNAL_PORT)
79
- flask_app.run(
80
- host="127.0.0.1",
81
- port=INTERNAL_PORT,
82
- debug=False,
83
- use_reloader=False,
84
- threaded=True,
85
- )
86
-
87
-
88
- # Start Flask as daemon thread before launching Gradio
89
- flask_thread = threading.Thread(target=start_flask_worker, daemon=True, name="Flask-Worker")
90
- flask_thread.start()
91
- time.sleep(1.0) # Brief grace period for socket binding
92
 
93
 
94
  # ---------------------------------------------------------------------------
95
- # 2. Gradio Business Logic & Handlers
96
  # ---------------------------------------------------------------------------
97
  def handle_stock_search(query: str) -> str:
98
- """Queries the internal search API for matching listed stocks."""
99
  if not query or len(query.strip()) < 2:
100
  return "⚠️ *Please enter at least 2 characters to search.*"
101
 
102
  try:
103
- url = f"http://127.0.0.1:{INTERNAL_PORT}/api/search?q={query.strip()}"
104
- resp = requests.get(url, timeout=10)
105
- if resp.status_code != 200:
106
- return f"❌ Search error ({resp.status_code}): {resp.text}"
107
-
108
- data = resp.json()
109
- if not data:
110
  return f"No listed companies found matching **'{query}'**."
111
 
112
  lines = ["| Symbol | Company Name | Industry |", "| :--- | :--- | :--- |"]
113
- for item in data[:15]:
114
  sym = item.get("symbol", "-")
115
  name = item.get("name", "-")
116
  ind = item.get("industry", "N/A")
@@ -119,7 +114,7 @@ def handle_stock_search(query: str) -> str:
119
  return "\n".join(lines)
120
  except Exception as exc:
121
  logger.error("Search failed: %s", exc)
122
- return f"❌ Internal search error: {exc}"
123
 
124
 
125
  def handle_generate_report(
@@ -127,54 +122,41 @@ def handle_generate_report(
127
  refresh: bool,
128
  progress=gr.Progress(),
129
  ) -> Tuple[Optional[str], str, str]:
130
- """Generates the institutional equity report and returns the compiled PDF."""
131
  if not symbol or not symbol.strip():
132
  return None, "⚠️ *Please specify a stock ticker (e.g. INFY, WIPRO, TCS, M&M).*", ""
133
 
134
- sym = symbol.strip().upper()
135
  progress(0.1, desc=f"Initializing report pipeline for {sym}...")
136
 
137
- api_url = f"http://127.0.0.1:{INTERNAL_PORT}/api/stocks/{sym}/report"
138
- if refresh:
139
- api_url += "?refresh=1"
140
-
141
- progress(0.3, desc=f"Executing quantitative checks and RAG extraction for {sym}...")
142
  try:
143
  t0 = time.perf_counter()
144
- resp = requests.get(api_url, timeout=400)
 
 
145
  elapsed = time.perf_counter() - t0
146
 
147
- if resp.status_code != 200:
148
- err_msg = f"❌ Report generation failed ({resp.status_code}):\n```json\n{resp.text}\n```"
149
- return None, err_msg, ""
150
-
151
- data = resp.json()
152
- drive_link = data.get("view_link") or data.get("drive_status") or "Drive not configured"
153
- was_built = data.get("generated", False)
154
-
155
- progress(0.85, desc=f"Fetching compiled PDF file bytes...")
156
- file_resp = requests.get(
157
- f"http://127.0.0.1:{INTERNAL_PORT}/api/stocks/{sym}/report/file",
158
- timeout=60,
159
- )
160
- if file_resp.status_code != 200:
161
- return None, f"❌ Failed to retrieve PDF bytes ({file_resp.status_code})", drive_link
162
-
163
- temp_pdf = os.path.join(os.path.expanduser("~"), f"{sym}_institutional_report.pdf")
164
- with open(temp_pdf, "wb") as f:
165
- f.write(file_resp.content)
166
 
167
  progress(1.0, desc="Report ready!")
168
 
 
 
169
  status_text = (
170
  f"✅ **Institutional Report Successfully Generated for `{sym}`!**\n\n"
171
- f"- **Execution Time**: `{elapsed:.2f}s` (Status: {'Compiled New' if was_built else 'Served from Cache'})\n"
172
- f"- **PDF File Size**: `{len(file_resp.content) / 1024:.1f} KB`\n"
173
  f"- **Google Drive Mirror**: {f'[{drive_link}]({drive_link})' if drive_link.startswith('http') else drive_link}"
174
  )
175
 
176
- drive_info = f"Google Drive: {drive_link}"
177
- return temp_pdf, status_text, drive_info
178
  except Exception as exc:
179
  logger.error("Report generation handler failed for %s: %s", sym, exc)
180
  return None, f"❌ Exception during generation: {exc}", ""
@@ -334,15 +316,10 @@ with gr.Blocks(title="GrowNXT Institutional Equity Platform", theme=gr.themes.So
334
  """)
335
 
336
  # ---------------------------------------------------------------------------
337
- # 4. Main Launch Entrypoint
338
  # ---------------------------------------------------------------------------
339
- if __name__ == "__main__":
340
- logger.info("Launching GrowNXT Gradio Platform on 0.0.0.0:%d...", HF_PORT)
341
- demo.launch(
342
- server_name="0.0.0.0",
343
- server_port=HF_PORT,
344
- show_api=False,
345
- share=False,
346
- )
347
-
348
 
 
 
 
 
1
  """GrowNXT Institutional Equity Research Platform -- Hugging Face Spaces & Local Entrypoint.
2
 
3
+ Combines FastAPI, Flask WSGI Middleware, and Gradio 5 into a single production server:
4
+ 1. REST API Endpoints on /api/* and /v1/* (Flask via WSGIMiddleware)
5
+ 2. Interactive Gradio UI on / (Report Generator, Stock Search, Live SLM Chat)
6
+ 3. Direct in-process execution for zero internal latency.
 
 
7
 
8
  Google Python Style Guide Compliant.
9
  """
 
13
  import json
14
  import logging
15
  import os
 
16
  import time
17
  from typing import Any, Dict, Generator, List, Optional, Tuple
18
 
 
20
  os.environ["GRADIO_SSR_MODE"] = "False"
21
 
22
  from dotenv import load_dotenv
23
+ from fastapi import FastAPI
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+ from starlette.middleware.wsgi import WSGIMiddleware
26
  import gradio as gr
27
  import requests
28
+ import uvicorn
29
 
30
  # Patch gradio_client bug with Pydantic v2 boolean additionalProperties schemas
31
  try:
 
48
  pass
49
 
50
  from api.app import create_app
51
+ from api.search import find
52
+ from core.config import OUTPUT_DIR, report_path, safe_ticker
53
  from core.llm_config import (
54
  ACTIVE_MODEL,
55
  DEFAULT_BASE_URL,
 
58
  clean_thinking_tokens,
59
  stream_llm_response,
60
  )
61
+ from reporting.engine import generate_report
62
+ from storage import gdrive
63
 
64
  load_dotenv()
65
 
 
69
  )
70
  logger = logging.getLogger("grownxt.spaces")
71
 
 
72
  HF_PORT = int(os.getenv("PORT", "7860"))
73
 
74
  # ---------------------------------------------------------------------------
75
+ # 1. Initialize Backend Applications
76
  # ---------------------------------------------------------------------------
77
  flask_app = create_app()
78
+ fastapi_app = FastAPI(title="GrowNXT API & Reverse Proxy", docs_url=None, redoc_url=None)
79
+
80
+ fastapi_app.add_middleware(
81
+ CORSMiddleware,
82
+ allow_origins=["*"],
83
+ allow_credentials=True,
84
+ allow_methods=["*"],
85
+ allow_headers=["*"],
86
+ )
87
 
88
+ # Mount Flask WSGI App on /api and /v1 for direct external REST access
89
+ wsgi_handler = WSGIMiddleware(flask_app)
90
+ fastapi_app.mount("/api", wsgi_handler)
91
+ fastapi_app.mount("/v1", wsgi_handler)
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
 
94
  # ---------------------------------------------------------------------------
95
+ # 2. Gradio Direct In-Process Handlers (Zero Latency)
96
  # ---------------------------------------------------------------------------
97
  def handle_stock_search(query: str) -> str:
98
+ """Performs direct in-process search for listed stocks."""
99
  if not query or len(query.strip()) < 2:
100
  return "⚠️ *Please enter at least 2 characters to search.*"
101
 
102
  try:
103
+ results = find(query.strip())
104
+ if not results:
 
 
 
 
 
105
  return f"No listed companies found matching **'{query}'**."
106
 
107
  lines = ["| Symbol | Company Name | Industry |", "| :--- | :--- | :--- |"]
108
+ for item in results[:15]:
109
  sym = item.get("symbol", "-")
110
  name = item.get("name", "-")
111
  ind = item.get("industry", "N/A")
 
114
  return "\n".join(lines)
115
  except Exception as exc:
116
  logger.error("Search failed: %s", exc)
117
+ return f"❌ Search error: {exc}"
118
 
119
 
120
  def handle_generate_report(
 
122
  refresh: bool,
123
  progress=gr.Progress(),
124
  ) -> Tuple[Optional[str], str, str]:
125
+ """Generates the institutional equity report in-process and returns the compiled PDF."""
126
  if not symbol or not symbol.strip():
127
  return None, "⚠️ *Please specify a stock ticker (e.g. INFY, WIPRO, TCS, M&M).*", ""
128
 
129
+ sym = safe_ticker(symbol.strip().upper())
130
  progress(0.1, desc=f"Initializing report pipeline for {sym}...")
131
 
 
 
 
 
 
132
  try:
133
  t0 = time.perf_counter()
134
+ progress(0.3, desc=f"Executing quantitative checks and RAG extraction for {sym}...")
135
+
136
+ pdf_path = generate_report(sym, output_dir=OUTPUT_DIR, refresh=refresh)
137
  elapsed = time.perf_counter() - t0
138
 
139
+ progress(0.85, desc="Checking Google Drive mirror...")
140
+ drive_link = "Drive not configured"
141
+ try:
142
+ if gdrive.credentials_present():
143
+ up = gdrive.ensure_uploaded(pdf_path, sym, force=refresh)
144
+ drive_link = up.view_link or "Uploaded"
145
+ except Exception as drive_exc:
146
+ logger.warning("Drive upload error: %s", drive_exc)
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  progress(1.0, desc="Report ready!")
149
 
150
+ file_size_kb = pdf_path.stat().st_size / 1024 if pdf_path.exists() else 0
151
+
152
  status_text = (
153
  f"✅ **Institutional Report Successfully Generated for `{sym}`!**\n\n"
154
+ f"- **Execution Time**: `{elapsed:.2f}s`\n"
155
+ f"- **PDF File Size**: `{file_size_kb:.1f} KB`\n"
156
  f"- **Google Drive Mirror**: {f'[{drive_link}]({drive_link})' if drive_link.startswith('http') else drive_link}"
157
  )
158
 
159
+ return str(pdf_path), status_text, drive_link
 
160
  except Exception as exc:
161
  logger.error("Report generation handler failed for %s: %s", sym, exc)
162
  return None, f"❌ Exception during generation: {exc}", ""
 
316
  """)
317
 
318
  # ---------------------------------------------------------------------------
319
+ # 4. Mount Gradio onto FastAPI App
320
  # ---------------------------------------------------------------------------
321
+ app = gr.mount_gradio_app(fastapi_app, demo, path="/")
 
 
 
 
 
 
 
 
322
 
323
+ if __name__ == "__main__":
324
+ logger.info("Launching GrowNXT Unified Server (FastAPI + Flask WSGI + Gradio) on 0.0.0.0:%d...", HF_PORT)
325
+ uvicorn.run(app, host="0.0.0.0", port=HF_PORT, log_level="info")
requirements.txt CHANGED
@@ -17,4 +17,6 @@ qdrant-client
17
  sentence-transformers
18
  huggingface-hub>=0.26.0
19
  gunicorn
 
 
20
  gradio>=5.16.0
 
17
  sentence-transformers
18
  huggingface-hub>=0.26.0
19
  gunicorn
20
+ fastapi>=0.110.0
21
+ uvicorn>=0.28.0
22
  gradio>=5.16.0