sathishaiuse commited on
Commit
f4146f5
·
verified ·
1 Parent(s): 1a7d3e3

Upload 8 files

Browse files
Files changed (8) hide show
  1. README.md +25 -17
  2. app.py +71 -0
  3. model.py +53 -0
  4. prompts.py +66 -0
  5. requirements.txt +10 -3
  6. runtime.txt +1 -0
  7. salesforce.py +41 -0
  8. schema.py +49 -0
README.md CHANGED
@@ -1,20 +1,28 @@
1
- ---
2
- title: Salesforce Generate Pre Call Summary
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: To generate pre call summary for agent
12
- license: apache-2.0
13
- ---
14
 
15
- # Welcome to Streamlit!
16
 
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ # Pre‑Call Summary Generator (Salesforce → Hugging Face Space)
3
 
4
+ A Streamlit app that:
5
+ 1) Accepts Salesforce credentials (instance URL + access token).
6
+ 2) Pulls Account, Opportunities, Cases via REST/SOQL.
7
+ 3) Calls a summarization **model** (Hugging Face Inference API by default).
8
+ 4) Shows a structured **pre‑call summary** and (optionally) writes it back to Salesforce as a Note.
9
+
10
+ ## How to run on Hugging Face Spaces
11
+
12
+ 1. Create a new Space → **SDK: Streamlit** → **Private** (recommended for prototypes).
13
+ 2. Upload this repo's files.
14
+ 3. In the Space → **Settings → Repository secrets**, add your tokens:
15
+ - `HF_TOKEN` : (required) A Hugging Face token with Inference API access.
16
+ 4. (Optional) For testing locally, you can run `streamlit run app.py`.
17
+
18
+ ## Security Notes
19
+ - Do **not** log or persist Salesforce tokens. This app keeps everything in memory only for the session.
20
+ - For production, prefer Salesforce OAuth Web Server flow instead of pasting tokens.
21
+ - Scope the SOQL fields and follow FLS/sharing best practices in your org.
22
+
23
+ ## Usage
24
+ - Enter `instance_url` (e.g., `https://yourDomain.my.salesforce.com`) and a valid **Access Token**.
25
+ - Provide an `Account Id` (e.g., starts with `001...`).
26
+ - Click **Generate Summary**.
27
+ - Optionally check **"Write to Salesforce as Note"** to save the JSON summary back.
28
 
 
 
app.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import streamlit as st
3
+ import json
4
+ from salesforce import get_context, create_note
5
+ from model import PreCallSummaryModel
6
+ from schema import PreCallSummary
7
+
8
+ st.set_page_config(page_title="Pre‑Call Summary Generator", layout="wide")
9
+
10
+ st.title("Pre‑Call Summary Generator (Salesforce → HF Space)")
11
+
12
+ with st.expander("Instructions", expanded=False):
13
+ st.markdown("""
14
+ 1. Enter your Salesforce **Instance URL** (e.g., `https://xxx.my.salesforce.com`).
15
+ 2. Provide a short‑lived **Access Token** (from a Connected App / OAuth flow).
16
+ 3. Provide an **Account Id** (`001...`).
17
+ 4. Click **Generate Summary**. Optionally save it as a **Note** in Salesforce.
18
+ **Security:** tokens are used only in memory for this session and are not logged.
19
+ """)
20
+
21
+ inst = st.text_input("Salesforce Instance URL", placeholder="https://yourDomain.my.salesforce.com")
22
+ token = st.text_input("Salesforce Access Token", type="password", placeholder="Paste a short‑lived token")
23
+ acct_id = st.text_input("Account Id", placeholder="001xxxxxxxxxxxx")
24
+
25
+ colA, colB, colC = st.columns([1,1,1])
26
+ with colA:
27
+ opp_n = st.number_input("Max Opportunities", min_value=0, max_value=20, value=5, step=1)
28
+ with colB:
29
+ case_n = st.number_input("Max Cases", min_value=0, max_value=20, value=5, step=1)
30
+ with colC:
31
+ task_n = st.number_input("Max Tasks", min_value=0, max_value=50, value=10, step=1)
32
+
33
+ push_back = st.checkbox("Write to Salesforce as Note after generation", value=False)
34
+ generate = st.button("Generate Summary", type="primary")
35
+
36
+ if generate:
37
+ if not inst or not token or not acct_id:
38
+ st.error("Please provide instance URL, access token, and account id.")
39
+ st.stop()
40
+
41
+ with st.spinner("Fetching Salesforce context..."):
42
+ try:
43
+ context = get_context(inst, token, acct_id, opp_n, case_n, task_n)
44
+ except Exception as e:
45
+ st.error(f"Salesforce API error: {e}")
46
+ st.stop()
47
+
48
+ st.subheader("Fetched Context (trimmed)")
49
+ st.json(context)
50
+
51
+ with st.spinner("Calling model..."):
52
+ model = PreCallSummaryModel()
53
+ try:
54
+ summary: PreCallSummary = model.generate(context)
55
+ except Exception as e:
56
+ st.error(f"Model error: {e}")
57
+ st.stop()
58
+
59
+ st.subheader("Pre‑Call Summary (JSON)")
60
+ st.json(json.loads(summary.model_dump_json()))
61
+
62
+ if push_back:
63
+ with st.spinner("Writing Note to Salesforce..."):
64
+ try:
65
+ note_id = create_note(inst, token, acct_id, "Pre‑Call Summary", summary.model_dump_json(indent=2))
66
+ st.success(f"Saved Note Id: {note_id}")
67
+ except Exception as e:
68
+ st.error(f"Failed to save Note: {e}")
69
+
70
+ st.download_button("Download JSON", data=summary.model_dump_json(indent=2),
71
+ file_name=f"precall_summary_{acct_id}.json", mime="application/json")
model.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import json, orjson, json5, requests, os
3
+ from typing import Dict, Any
4
+ from schema import ensure_schema, PreCallSummary
5
+ from prompts import build_prompt
6
+
7
+ class PreCallSummaryModel:
8
+ def __init__(self, inference_model_url: str | None = None, hf_token: str | None = None):
9
+ # If inference_model_url is None, we'll default to FLAN-T5-base via Inference API
10
+ self.inference_model_url = inference_model_url or "https://api-inference.huggingface.co/models/google/flan-t5-base"
11
+ self.hf_token = hf_token or os.environ.get("HF_TOKEN")
12
+
13
+ def _call_inference_api(self, prompt: str) -> str:
14
+ headers = {"Authorization": f"Bearer {self.hf_token}"} if self.hf_token else {}
15
+ payload = {"inputs": prompt, "parameters": {"max_new_tokens": 512, "return_full_text": False}}
16
+ r = requests.post(self.inference_model_url, headers=headers, json=payload, timeout=120)
17
+ r.raise_for_status()
18
+ data = r.json()
19
+ if isinstance(data, list) and data and "generated_text" in data[0]:
20
+ return data[0]["generated_text"]
21
+ if isinstance(data, dict) and "generated_text" in data:
22
+ return data["generated_text"]
23
+ # Fallback: some endpoints return plain text
24
+ return str(data)
25
+
26
+ def _parse_json(self, s: str) -> Dict[str, Any]:
27
+ # Try strict json, then orjson, then json5, then best-effort substring extraction
28
+ try:
29
+ return json.loads(s)
30
+ except Exception:
31
+ pass
32
+ try:
33
+ return orjson.loads(s)
34
+ except Exception:
35
+ pass
36
+ try:
37
+ return json5.loads(s)
38
+ except Exception:
39
+ pass
40
+ # naive extraction between first { and last }
41
+ try:
42
+ start, end = s.find("{"), s.rfind("}")
43
+ if start != -1 and end != -1 and end > start:
44
+ return json.loads(s[start:end+1])
45
+ except Exception:
46
+ pass
47
+ return {}
48
+
49
+ def generate(self, context: Dict[str, Any]) -> PreCallSummary:
50
+ prompt = build_prompt(json.dumps(context, ensure_ascii=False, default=str))
51
+ raw = self._call_inference_api(prompt)
52
+ parsed = self._parse_json(raw)
53
+ return ensure_schema(parsed)
prompts.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ SCHEMA_TEXT = """
3
+ Return JSON with EXACTLY these keys:
4
+ {
5
+ "client_overview": str,
6
+ "relationship_timeline": [
7
+ {"date":"YYYY-MM-DD","event":"...", "source":"Case|Opportunity|Task|Activity|Other"}
8
+ ],
9
+ "open_items": [str],
10
+ "opportunities": [
11
+ {"name":"...", "stage":"...", "amount": number, "closeDate":"YYYY-MM-DD or null", "nextStep":"..."}
12
+ ],
13
+ "risks_flags": [str],
14
+ "recommendations": [str],
15
+ "pre_call_questions": [str],
16
+ "sources": [str]
17
+ }
18
+ """
19
+
20
+ FEW_SHOT = [
21
+ {
22
+ "context": {
23
+ "account": {"Id":"001X","Name":"Acme Corp","Industry":"Manufacturing","Type":"Customer-Direct","Rating":"Hot","AnnualRevenue": 45000000},
24
+ "opportunities": [
25
+ {"Id":"006A","Name":"Q4 Renewal","StageName":"Negotiation","Amount": 2000000, "CloseDate":"2025-09-30", "NextStep":"Legal review"},
26
+ {"Id":"006B","Name":"Cross-sell Treasury","StageName":"Qualification","Amount": 500000, "CloseDate":"2025-10-15", "NextStep":"Intro call"}
27
+ ],
28
+ "cases": [
29
+ {"Id":"500C","CaseNumber":"000123","Subject":"Payment delay","Status":"In Progress","Priority":"High","Origin":"Phone","LastModifiedDate":"2025-08-01T12:00:00Z"}
30
+ ],
31
+ "activities": [
32
+ {"Id":"00T1","Subject":"Quarterly check-in","ActivityDate":"2025-07-15","Status":"Completed","Owner":{"Alias":"jdoe"}}
33
+ ]
34
+ },
35
+ "output": {
36
+ "client_overview": "Acme Corp (Manufacturing, Customer-Direct). Est. revenue $45M. Relationship rated Hot.",
37
+ "relationship_timeline": [
38
+ {"date":"2025-08-01","event":"High-priority case: Payment delay is in progress.","source":"Case"},
39
+ {"date":"2025-07-15","event":"Completed quarterly check-in.","source":"Activity"}
40
+ ],
41
+ "open_items": ["Follow up on payment delay case"],
42
+ "opportunities": [
43
+ {"name":"Q4 Renewal","stage":"Negotiation","amount":2000000,"closeDate":"2025-09-30","nextStep":"Legal review"},
44
+ {"name":"Cross-sell Treasury","stage":"Qualification","amount":500000,"closeDate":"2025-10-15","nextStep":"Intro call"}
45
+ ],
46
+ "risks_flags": ["Open high-priority service issue may affect renewal"],
47
+ "recommendations": ["Confirm payment issue resolution","Schedule renewal legal review","Propose treasury intro"],
48
+ "pre_call_questions": ["Who is sponsor for renewal?","Any blockers from finance?","Treasury appetite and timeline?"],
49
+ "sources": ["Account:001X","Opportunity:006A","Opportunity:006B","Case:500C","Task:00T1"]
50
+ }
51
+ }
52
+ ]
53
+
54
+ def build_prompt(context_json: str) -> str:
55
+ # Guidance prompt for instruction-tuned models like FLAN-T5
56
+ parts = []
57
+ parts.append("You are a banking relationship assistant. Summarize pre-call context for the given Account.")
58
+ parts.append("Be concise and factual. If data is missing, say 'Unknown' briefly. Output STRICT JSON only.")
59
+ parts.append(SCHEMA_TEXT)
60
+ parts.append("Here is a worked example:")
61
+ parts.append("Context: " + str(FEW_SHOT[0]["context"]))
62
+ parts.append("Output JSON: " + str(FEW_SHOT[0]["output"]))
63
+ parts.append("Now produce output for this Context:")
64
+ parts.append(context_json)
65
+ parts.append("Output JSON only:")
66
+ return "\n\n".join(parts)
requirements.txt CHANGED
@@ -1,3 +1,10 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
1
+
2
+ streamlit==1.37.0
3
+ requests==2.32.3
4
+ pydantic==2.8.2
5
+ orjson==3.10.7
6
+ json5==0.9.25
7
+ # If you later want local models instead of Inference API, uncomment below
8
+ # transformers==4.43.3
9
+ # torch>=2.2.0
10
+ # sentencepiece==0.2.0
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.10.14
salesforce.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import requests
3
+ from typing import Dict, Any
4
+
5
+ def sf_query(instance_url: str, access_token: str, soql: str) -> list[dict]:
6
+ api = f"{instance_url}/services/data/v61.0/query"
7
+ res = requests.get(api, params={"q": soql},
8
+ headers={"Authorization": f"Bearer {access_token}"})
9
+ res.raise_for_status()
10
+ return res.json().get("records", [])
11
+
12
+ def get_context(instance_url: str, access_token: str, account_id: str,
13
+ limit_opps: int = 5, limit_cases: int = 5, limit_tasks: int = 10) -> Dict[str, Any]:
14
+ acct = sf_query(instance_url, access_token,
15
+ f"SELECT Id, Name, Industry, Type, Rating, AnnualRevenue, Owner.Alias, BillingCountry "
16
+ f"FROM Account WHERE Id='{account_id}' LIMIT 1")
17
+ opps = sf_query(instance_url, access_token,
18
+ f"SELECT Id, Name, StageName, Amount, CloseDate, NextStep, Owner.Alias "
19
+ f"FROM Opportunity WHERE AccountId='{account_id}' ORDER BY LastModifiedDate DESC LIMIT {limit_opps}")
20
+ cases = sf_query(instance_url, access_token,
21
+ f"SELECT Id, CaseNumber, Subject, Status, Priority, Origin, LastModifiedDate "
22
+ f"FROM Case WHERE AccountId='{account_id}' ORDER BY LastModifiedDate DESC LIMIT {limit_cases}")
23
+ tasks = sf_query(instance_url, access_token,
24
+ f"SELECT Id, Subject, ActivityDate, Status, Owner.Alias "
25
+ f"FROM Task WHERE WhatId='{account_id}' OR AccountId='{account_id}' "
26
+ f"ORDER BY ActivityDate DESC LIMIT {limit_tasks}")
27
+ return {
28
+ "account": acct[0] if acct else {},
29
+ "opportunities": opps,
30
+ "cases": cases,
31
+ "activities": tasks
32
+ }
33
+
34
+ def create_note(instance_url: str, access_token: str, account_id: str, title: str, body: str) -> str:
35
+ payload = {"Title": title, "Body": body, "ParentId": account_id}
36
+ res = requests.post(f"{instance_url}/services/data/v61.0/sobjects/Note",
37
+ headers={"Authorization": f"Bearer {access_token}",
38
+ "Content-Type": "application/json"},
39
+ json=payload)
40
+ res.raise_for_status()
41
+ return res.json().get("id")
schema.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from pydantic import BaseModel, Field, ValidationError
3
+ from typing import List, Optional
4
+
5
+ class TimelineItem(BaseModel):
6
+ date: str = Field(..., description="YYYY-MM-DD")
7
+ event: str
8
+ source: str
9
+
10
+ class OpportunityItem(BaseModel):
11
+ name: str
12
+ stage: str
13
+ amount: float
14
+ closeDate: Optional[str] = None
15
+ nextStep: Optional[str] = None
16
+
17
+ class PreCallSummary(BaseModel):
18
+ client_overview: str
19
+ relationship_timeline: List[TimelineItem]
20
+ open_items: List[str]
21
+ opportunities: List[OpportunityItem]
22
+ risks_flags: List[str]
23
+ recommendations: List[str]
24
+ pre_call_questions: List[str]
25
+ sources: List[str]
26
+
27
+ def ensure_schema(d: dict) -> PreCallSummary:
28
+ """Validate and coerce a dict to PreCallSummary, filling missing keys with safe defaults."""
29
+ defaults = {
30
+ "client_overview": "",
31
+ "relationship_timeline": [],
32
+ "open_items": [],
33
+ "opportunities": [],
34
+ "risks_flags": [],
35
+ "recommendations": [],
36
+ "pre_call_questions": [],
37
+ "sources": []
38
+ }
39
+ merged = {**defaults, **(d or {})}
40
+ try:
41
+ return PreCallSummary.model_validate(merged)
42
+ except ValidationError as e:
43
+ # Best-effort repair for common issues
44
+ for fld in ["relationship_timeline", "open_items", "opportunities", "risks_flags", "recommendations", "pre_call_questions", "sources"]:
45
+ if merged.get(fld) is None:
46
+ merged[fld] = []
47
+ if merged.get("client_overview") is None:
48
+ merged["client_overview"] = ""
49
+ return PreCallSummary.model_validate(merged)