VeuReu commited on
Commit
ba54b37
·
verified ·
1 Parent(s): 2b5f219

Upload 12 files

Browse files
README.md CHANGED
@@ -1,26 +1,24 @@
1
  ---
2
- title: veureu-stools
3
- emoji: 🛠️
4
- colorFrom: yellow
5
- colorTo: yellow
6
  sdk: gradio
7
  sdk_version: "4.44.1"
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- # 🛠️ veureu-stools (Salamandra-7B-Tools · ZeroGPU)
13
 
14
  ## Endpoints
15
- - **`/api/predict`** (Gradio): entrada `[ "<messages_json>", "<tools_json>" ]` → salida `{ "text": "...", "tool_calls": [...], "tool_results": [...] }`.
16
  ➜ Este es el endpoint que usa el Space **engine**.
17
- - **`/api/chat`** (Gradio): entrada `[ "<messages_json>", "<tools_json>", max_new_tokens, temperature, top_p ]` → salida idéntica.
18
 
19
  ### Variables de entorno
20
- - `MODEL_ID` (opcional): por defecto `BSC-LT/salamandra-7b-tools`.
21
- Puedes apuntar a `BSC-LT/salamandra-7b-instruct` si prefieres.
22
 
23
  ### Notas
24
- - El modelo **no ejecuta** herramientas reales salvo un **ejemplo local**: `calculator` (seguro).
25
- Si el modelo devuelve `{"tool_calls":[...]}`, el Space intentará ejecutar esas llamadas en sandbox y añadirá `tool_results`.
26
- Puedes desactivar la ejecución poniendo `EXECUTE_TOOLS=False` en `app.py`.
 
1
  ---
2
+ title: veureu-schat
3
+ emoji: 💬
4
+ colorFrom: purple
5
+ colorTo: red
6
  sdk: gradio
7
  sdk_version: "4.44.1"
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # 💬 veureu-schat (Salamandra-7B-Instruct · ZeroGPU)
13
 
14
  ## Endpoints
15
+ - **`/api/predict`** (Gradio): entrada `["<prompt>"]` → salida `"<texto>"`.
16
  ➜ Este es el endpoint que usa el Space **engine**.
17
+ - **`/api/generate`** (Gradio): entrada `[prompt, system, max_new_tokens, temperature, top_p]` → salida `"<texto>"`.
18
 
19
  ### Variables de entorno
20
+ - `MODEL_ID` (opcional): por defecto `BSC-LT/salamandra-7b-instruct`.
 
21
 
22
  ### Notas
23
+ - El modelo usa `chat_template` si existe; si no, se compone un prompt clásico con bloque `system`.
24
+ - GPU: se activa con `@spaces.GPU` automáticamente (ZeroGPU).
 
app.py CHANGED
@@ -103,11 +103,11 @@ with gr.Blocks(title="Salamandra 7B Instruct · ZeroGPU") as demo:
103
  with gr.Column(scale=1):
104
  out = gr.Textbox(label="Respuesta", lines=18)
105
 
106
- btn.click(generate_advanced, [in_prompt, in_system, max_new, temp, top_p], out, api_name="generate")
107
 
108
  # Endpoint minimalista compatible con el ENGINE (/predict: solo prompt)
109
  in_prompt_engine = gr.Textbox(label="Prompt (ENGINE)", value="Di hola en una frase.")
110
  out_engine = gr.Textbox(label="Respuesta (ENGINE)")
111
- gr.Button("Probar /predict").click(predict_for_engine, [in_prompt_engine], out_engine, api_name="predict")
112
 
113
- demo.queue(concurrency_count=1, max_size=16).launch()
 
103
  with gr.Column(scale=1):
104
  out = gr.Textbox(label="Respuesta", lines=18)
105
 
106
+ btn.click(generate_advanced, [in_prompt, in_system, max_new, temp, top_p], out, api_name="generate", concurrency_limit=1)
107
 
108
  # Endpoint minimalista compatible con el ENGINE (/predict: solo prompt)
109
  in_prompt_engine = gr.Textbox(label="Prompt (ENGINE)", value="Di hola en una frase.")
110
  out_engine = gr.Textbox(label="Respuesta (ENGINE)")
111
+ gr.Button("Probar /predict").click(predict_for_engine, [in_prompt_engine], out_engine, api_name="predict", concurrency_limit=1)
112
 
113
+ demo.queue(max_size=16).launch()
engine-ddo/.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.sqlite3
3
+ *.db
4
+ *.pyc
5
+ .env
6
+ /data/
engine-ddo/Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Copy the dependencies file to the working directory
8
+ COPY requirements.txt .
9
+
10
+ # Install any needed packages specified in requirements.txt
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy the rest of the application code to the working directory
14
+ COPY . .
15
+
16
+ # Expose the port the app runs on
17
+ EXPOSE 7860
18
+
19
+ # Run the application
20
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
engine-ddo/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Engine (Due Diligence Optimization)
3
+ emoji: ⚙️
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # engine-ddo (FastAPI)
11
+
12
+
13
+ FastAPI backend for Due Diligence Optimization demo. Persists data in SQLite and exposes REST endpoints for:
14
+ - Products: ingest from PDFs, list, detail
15
+ - Customers: list/update
16
+ - Interactions: chat with persistence (conversations/messages)
17
+
18
+
19
+ ## Run locally
20
+ ```bash
21
+ uvicorn main:app --host 0.0.0.0 --port 7860 --reload
engine-ddo/db.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import sessionmaker, DeclarativeBase
3
+ import os
4
+
5
+
6
+ DB_PATH = os.environ.get("DDO_DB_PATH", "./ddo.sqlite3")
7
+ engine = create_engine(f"sqlite:///{DB_PATH}", connect_args={"check_same_thread": False})
8
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
9
+
10
+
11
+ class Base(DeclarativeBase):
12
+ pass
13
+
14
+
15
+ def get_db():
16
+ db = SessionLocal()
17
+ try:
18
+ yield db
19
+ finally:
20
+ db.close()
engine-ddo/main.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI, UploadFile, File, Depends, HTTPException, Form
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from sqlalchemy.orm import Session
5
+ from db import Base, engine, get_db
6
+ from models import Product, CustomerProfile
7
+ from schemas import (
8
+ ProductOut, CustomerOut, CustomerUpdate,
9
+ ChatRequest, ChatResponse, ConversationOut
10
+ )
11
+ from services import extract_and_upsert_products_from_llm, ensure_default_customers, get_or_create_conversation, add_message, get_history
12
+ from typing import List
13
+ import tempfile
14
+
15
+ app = FastAPI(title="engine-ddo", openapi_url="/openapi.json")
16
+
17
+ # CORS for Streamlit UI Space
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ # Initialize DB
27
+ Base.metadata.create_all(bind=engine)
28
+
29
+ @app.get("/health")
30
+ def health():
31
+ return {"status": "ok"}
32
+
33
+ # -------- PRODUCTS --------
34
+ @app.post("/products/ingest", response_model=List[ProductOut])
35
+ async def ingest_products(public_offering: UploadFile = File(...), private_notes: UploadFile = File(...), db: Session = Depends(get_db)):
36
+ # Save temp files to pass paths to the service
37
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as f1:
38
+ f1.write(await public_offering.read())
39
+ public_path = f1.name
40
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as f2:
41
+ f2.write(await private_notes.read())
42
+ notes_path = f2.name
43
+
44
+ try:
45
+ # Call the new service to process PDFs with an LLM
46
+ extract_and_upsert_products_from_llm(db, public_path, notes_path)
47
+ finally:
48
+ # Clean up the temporary files
49
+ os.remove(public_path)
50
+ os.remove(notes_path)
51
+
52
+ # Return all products from the database
53
+ rows = db.query(Product).order_by(Product.name.asc()).all()
54
+ return rows
55
+
56
+ @app.get("/products/list", response_model=List[ProductOut])
57
+ def list_products(db: Session = Depends(get_db)):
58
+ rows = db.query(Product).order_by(Product.name.asc()).all()
59
+ return rows
60
+
61
+ # -------- CUSTOMERS --------
62
+ @app.get("/customers/list", response_model=List[CustomerOut])
63
+ def list_customers(db: Session = Depends(get_db)):
64
+ ensure_default_customers(db)
65
+ rows = db.query(CustomerProfile).order_by(CustomerProfile.name.asc()).all()
66
+ return rows
67
+
68
+ @app.post("/customers/update", response_model=CustomerOut)
69
+ def update_customer(payload: CustomerUpdate, db: Session = Depends(get_db)):
70
+ row = db.query(CustomerProfile).filter_by(name=payload.name).first()
71
+ if not row:
72
+ row = CustomerProfile(name=payload.name)
73
+ db.add(row)
74
+ if payload.attributes is not None:
75
+ row.attributes = payload.attributes
76
+ if payload.wcltv is not None:
77
+ row.wcltv = payload.wcltv
78
+ if payload.n is not None:
79
+ row.n = payload.n
80
+ db.commit()
81
+ db.refresh(row)
82
+ return row
83
+
84
+ # -------- INTERACTIONS (chat) --------
85
+
86
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
87
+ MODEL = os.environ.get("LLM_MODEL", "gpt-4o-mini")
88
+
89
+ async def llm_reply(system_prompt: str, history: list, user_text: str) -> str:
90
+ """Return a reply from an external LLM if OPENAI_API_KEY set, else a rule-based stub."""
91
+ if OPENAI_API_KEY:
92
+ try:
93
+ from openai import OpenAI
94
+ client = OpenAI(api_key=OPENAI_API_KEY)
95
+ messages = [{"role": "system", "content": system_prompt}] + history + [{"role": "user", "content": user_text}]
96
+ resp = client.chat.completions.create(model=MODEL, messages=messages)
97
+ return resp.choices[0].message.content.strip()
98
+ except Exception as e:
99
+ return f"[LLM error fallback] I couldn't reach the model ({e}). Let's continue anyway."
100
+ # Fallback deterministic reply for demo
101
+ return "Thanks for the details! Could you share your main need, budget, and timeline? I can match a product for you."
102
+
103
+ @app.post("/interactions/chat", response_model=ChatResponse)
104
+ async def chat(req: ChatRequest, db: Session = Depends(get_db)):
105
+ profile = req.profile_name or "random"
106
+ convo = get_or_create_conversation(db, profile)
107
+ add_message(db, convo.id, sender="customer", text=req.user_text)
108
+
109
+ # Build history for LLM
110
+ hist = []
111
+ for turn in get_history(db, convo.id):
112
+ role = "user" if turn["sender"] == "customer" else "assistant"
113
+ hist.append({"role": role, "content": turn["text"]})
114
+
115
+ system_prompt = (
116
+ "You are a helpful sales assistant. Keep answers short, ask clarifying questions, and reference products generically."
117
+ )
118
+ reply = await llm_reply(system_prompt, hist, req.user_text)
119
+ add_message(db, convo.id, sender="agent", text=reply)
120
+
121
+ return {"reply": reply, "conversation_id": convo.id}
122
+
123
+ @app.get("/interactions/history", response_model=ConversationOut)
124
+ async def history(profile_name: str, db: Session = Depends(get_db)):
125
+ convo = get_or_create_conversation(db, profile_name)
126
+ hist = get_history(db, convo.id)
127
+ return {"id": convo.id, "profile_name": profile_name, "history": hist}
engine-ddo/models.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Text, Float, ForeignKey, DateTime
2
+ from sqlalchemy.orm import relationship
3
+ from datetime import datetime
4
+ from db import Base
5
+
6
+
7
+ class Product(Base):
8
+ __tablename__ = "products"
9
+ id = Column(Integer, primary_key=True)
10
+ name = Column(String(200), unique=True, index=True)
11
+ description = Column(Text, nullable=True)
12
+ notes = Column(Text, nullable=True)
13
+ price = Column(Float, nullable=True)
14
+
15
+
16
+ class CustomerProfile(Base):
17
+ __tablename__ = "customer_profiles"
18
+ id = Column(Integer, primary_key=True)
19
+ name = Column(String(200), unique=True, index=True)
20
+ attributes = Column(Text) # JSON or plain text description
21
+ wcltv = Column(Float, default=0.0)
22
+ n = Column(Integer, default=0)
23
+
24
+
25
+ class Conversation(Base):
26
+ __tablename__ = "conversations"
27
+ id = Column(Integer, primary_key=True)
28
+ profile_name = Column(String(200), index=True)
29
+ started_at = Column(DateTime, default=datetime.utcnow)
30
+ messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")
31
+
32
+
33
+ class Message(Base):
34
+ __tablename__ = "messages"
35
+ id = Column(Integer, primary_key=True)
36
+ conversation_id = Column(Integer, ForeignKey("conversations.id"))
37
+ sender = Column(String(32)) # 'customer' or 'agent'
38
+ text = Column(Text)
39
+ ts = Column(DateTime, default=datetime.utcnow)
40
+ conversation = relationship("Conversation", back_populates="messages")
engine-ddo/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.1
3
+ pydantic==2.8.2
4
+ SQLAlchemy==2.0.31
5
+ python-multipart==0.0.9
6
+ httpx==0.27.0
7
+ PyPDF2==3.0.1
8
+ openai==1.43.0 # optional; used only if OPENAI_API_KEY provided
engine-ddo/schemas.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import List, Optional
3
+
4
+
5
+ class ProductOut(BaseModel):
6
+ id: int
7
+ name: str
8
+ description: Optional[str] = None
9
+ notes: Optional[str] = None
10
+ price: Optional[float] = None
11
+
12
+ class Config:
13
+ from_attributes = True
14
+
15
+
16
+ class CustomerOut(BaseModel):
17
+ id: int
18
+ name: str
19
+ attributes: str
20
+ wcltv: float
21
+ n: int
22
+
23
+ class Config:
24
+ from_attributes = True
25
+
26
+
27
+ class CustomerUpdate(BaseModel):
28
+ name: str
29
+ attributes: Optional[str] = None
30
+ wcltv: Optional[float] = None
31
+ n: Optional[int] = None
32
+
33
+
34
+ class ChatTurn(BaseModel):
35
+ sender: str = Field(pattern="^(customer|agent)$")
36
+ text: str
37
+
38
+
39
+ class ConversationOut(BaseModel):
40
+ id: int
41
+ profile_name: str
42
+ history: List[ChatTurn]
43
+
44
+
45
+ class ChatRequest(BaseModel):
46
+ profile_name: str
47
+ user_text: str
48
+
49
+
50
+ class ChatResponse(BaseModel):
51
+ reply: str
52
+ conversation_id: int
engine-ddo/services.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from sqlalchemy.orm import Session
4
+ from models import Product, CustomerProfile, Conversation, Message
5
+ from typing import List
6
+ from PyPDF2 import PdfReader
7
+
8
+ # Product services
9
+
10
+ def _read_pdf_text(file_path: str) -> str:
11
+ try:
12
+ reader = PdfReader(file_path)
13
+ return "\n".join(page.extract_text() or "" for page in reader.pages)
14
+ except Exception:
15
+ return ""
16
+
17
+ def extract_and_upsert_products_from_llm(db: Session, public_pdf_path: str, private_pdf_path: str):
18
+ """Extracts product info from PDFs using an LLM and saves to DB."""
19
+ public_text = _read_pdf_text(public_pdf_path)
20
+ private_text = _read_pdf_text(private_pdf_path)
21
+
22
+ if not public_text and not private_text:
23
+ # Fallback for demo if PDFs are empty or unreadable
24
+ demo_products = [
25
+ Product(name="Demo Basic", description="Standard features for small teams.", notes="High churn risk.", price=9.0),
26
+ Product(name="Demo Pro", description="Advanced features and priority support.", notes="Stable customer base.", price=39.0),
27
+ Product(name="Demo Enterprise", description="Dedicated support and custom integrations.", notes="Potential for expansion.", price=199.0),
28
+ ]
29
+ for p in demo_products:
30
+ db.merge(p)
31
+ db.commit()
32
+ return
33
+
34
+ # Use the LLM call logic from main.py
35
+ from main import llm_reply
36
+
37
+ system_prompt = """
38
+ You are an expert data extractor. Your task is to analyze two documents, a public offering and a private notes document, and extract product information.
39
+
40
+ Respond with a single JSON array of objects. Each object should represent a product and have the following fields:
41
+ - "product": The name of the product.
42
+ - "description": The description from the public offering document.
43
+ - "notes": Internal notes from the private notes document.
44
+ - "price": The price as a numeric value (float), if available.
45
+
46
+ If you find information that does not belong to a specific product, assign it to a product named "general".
47
+ Ensure your output is a valid JSON array.
48
+ """
49
+
50
+ user_prompt = f"""
51
+ Here is the content from the public offering document:
52
+ --- PUBLIC OFFERING ---
53
+ {public_text}
54
+
55
+ Here is the content from the private notes document:
56
+ --- PRIVATE NOTES ---
57
+ {private_text}
58
+
59
+ Please extract the product information as a JSON array.
60
+ """
61
+
62
+ # This is a blocking call, so we don't need async here
63
+ import asyncio
64
+ llm_response = asyncio.run(llm_reply(system_prompt, [], user_prompt))
65
+
66
+ try:
67
+ # Clean the response to get only the JSON part
68
+ json_str = llm_response[llm_response.find('['):llm_response.rfind(']')+1]
69
+ extracted_data = json.loads(json_str)
70
+
71
+ for item in extracted_data:
72
+ product = Product(
73
+ name=item.get("product", "general"),
74
+ description=item.get("description"),
75
+ notes=item.get("notes"),
76
+ price=float(item["price"]) if item.get("price") else None
77
+ )
78
+ # Use merge to insert or update based on the primary key (name)
79
+ db.merge(product)
80
+ db.commit()
81
+
82
+ except (json.JSONDecodeError, TypeError, KeyError) as e:
83
+ # Handle cases where LLM output is not as expected
84
+ # For demo, we can log the error and maybe insert a placeholder
85
+ print(f"Error parsing LLM response: {e}")
86
+ placeholder = Product(name="Parsing Error", description="Could not parse data from documents.", notes=str(llm_response))
87
+ db.merge(placeholder)
88
+ db.commit()
89
+
90
+ # Customer services
91
+
92
+ def ensure_default_customers(db: Session):
93
+ defaults = [
94
+ ("random", "Synthetic profile with randomized traits", 0.0, 0),
95
+ ("SMB buyer", "Budget-conscious, quick decisions", 1200.0, 85),
96
+ ("Enterprise buyer", "Long sales cycle, security-focused", 24000.0, 12),
97
+ ]
98
+ for name, attrs, w, n in defaults:
99
+ row = db.query(CustomerProfile).filter_by(name=name).first()
100
+ if not row:
101
+ db.add(CustomerProfile(name=name, attributes=attrs, wcltv=w, n=n))
102
+ db.commit()
103
+
104
+ # Chat services
105
+
106
+ def get_or_create_conversation(db: Session, profile_name: str) -> Conversation:
107
+ convo = (
108
+ db.query(Conversation)
109
+ .filter_by(profile_name=profile_name)
110
+ .order_by(Conversation.id.desc())
111
+ .first()
112
+ )
113
+ if not convo:
114
+ convo = Conversation(profile_name=profile_name)
115
+ db.add(convo)
116
+ db.commit()
117
+ db.refresh(convo)
118
+ return convo
119
+
120
+
121
+ def add_message(db: Session, conversation_id: int, sender: str, text: str):
122
+ msg = Message(conversation_id=conversation_id, sender=sender, text=text)
123
+ db.add(msg)
124
+ db.commit()
125
+
126
+
127
+ def get_history(db: Session, conversation_id: int):
128
+ convo = db.query(Conversation).filter_by(id=conversation_id).first()
129
+ if not convo:
130
+ return []
131
+ return [{"sender": m.sender, "text": m.text} for m in convo.messages]
requirements.txt CHANGED
@@ -1,5 +1,3 @@
1
- gradio>=4.44.1
2
- spaces>=0.25.0
3
  transformers>=4.44.0
4
  torch>=2.2
5
  accelerate>=0.30.0
 
 
 
1
  transformers>=4.44.0
2
  torch>=2.2
3
  accelerate>=0.30.0