Skip to content

Salesforce Digest Agent#23

Open
parv15 wants to merge 1 commit into
scalekit-developers:mainfrom
parv15:salesforce-backed-customer-agent
Open

Salesforce Digest Agent#23
parv15 wants to merge 1 commit into
scalekit-developers:mainfrom
parv15:salesforce-backed-customer-agent

Conversation

@parv15

@parv15 parv15 commented Oct 28, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Salesforce-backed customer insight agent that automatically pulls updated customer and opportunity records from Salesforce and posts concise digests to Slack.
    • Configurable lookback window, PII redaction options, change detection to report only modifications, and optional custom summarization support.
  • Documentation

    • Added comprehensive configuration and usage guide.

@coderabbitai

coderabbitai Bot commented Oct 28, 2025

Copy link
Copy Markdown

Walkthrough

Introduces a complete Salesforce-to-Slack customer insights agent that orchestrates periodic fetches of Account and Opportunity records, applies optional redaction, detects changes via snapshots, and posts text-only digests with deep links to Slack, supported by Scalekit integration and configurable settings.

Changes

Cohort / File(s) Change Summary
Project scaffolding & configuration
.env.example, .gitignore, requirements.txt, README.md
Environment template, ignore patterns (secrets, Python bytecode, venvs, logs), dependency declarations (python-dotenv, pydantic, scalekit-sdk-python), and comprehensive project documentation including setup, workflow, configuration guide, and troubleshooting.
Configuration & validation
settings.py
Loads environment variables via dotenv; defines Settings class with typed configuration fields (URLs, identifiers, domain, file paths, limits, redaction flags); includes validate() classmethod to check required fields and raise ValueError if missing.
Scalekit connector & OAuth
sk_connectors.py
Singleton ScalekitConnector class wrapping Scalekit SDK; provides methods for OAuth (is_service_connected, get_authorization_url), action execution with retry/backoff (execute_action_with_retry), connector name inference, and robust error handling with parameter sanitization for logging.
Salesforce-to-Slack orchestration
sf_customer_360.py
Main workflow run_customer_360() that fetches Account/Opportunity records within lookback window, applies PII redaction, computes deltas against snapshots, invokes optional custom summarizer, builds text-only digest with Salesforce deep links, and posts to Slack; includes per-record formatting and SOQL execution with fallbacks.
Utility functions
sf_utils.py
Time utilities (iso_now, to_iso, hours_ago), PII redaction with regex patterns (redact_pii), snapshot persistence (load_snapshot, save_snapshot) with graceful error handling.
Slack block helpers
slack_blocks.py
Typed helper functions to construct Slack blocks: section_mrkdwn, divider, account_block, opp_line for formatting Account and Opportunity display elements.
Custom summarizer hook
custom_summarizer.py
Optional module with public function summarize_digest(accounts, opportunities) returning Optional[str]; enables custom LLM-based or heuristic summaries prepended to Slack messages; includes docstring with usage and security notes.

Sequence Diagram

sequenceDiagram
    participant Main as run_customer_360()
    participant SK as ScalekitConnector
    participant SF as Salesforce<br/>(via Scalekit)
    participant Slack as Slack<br/>(via Scalekit)
    participant Custom as custom_summarizer
    participant Snap as Snapshot File

    Main->>SK: Check Salesforce connection
    alt Not connected
        SK-->>Main: Return auth URL
        Note over Main: Emit OAuth link for user
    end
    
    Main->>SK: Check Slack connection
    alt Not connected
        SK-->>Main: Return auth URL
        Note over Main: Emit OAuth link for user
    end
    
    Main->>SK: Get API limits
    SK->>SF: salesforce_limits_get()
    SF-->>SK: Limits data
    SK-->>Main: Limits dict
    
    Main->>SF: fetch_accounts(since_iso, until_iso)
    SF-->>Main: Account records
    
    Main->>SF: fetch_opps(since_iso, until_iso)
    SF-->>Main: Opportunity records
    
    rect rgba(200, 220, 255, 0.3)
        Note over Main: Apply redaction<br/>(if enabled)
    end
    
    Main->>Snap: load_snapshot()
    Snap-->>Main: Previous state
    
    rect rgba(255, 220, 200, 0.3)
        Note over Main: Compute deltas<br/>(new/changed records)
    end
    
    alt Has changes
        Main->>Custom: summarize_digest(accts, opps)
        alt Custom available
            Custom-->>Main: Summary text (or None)
        else Error/Missing
            Main-->>Main: Skip summary
        end
        
        Main->>Main: build_text_fallback()<br/>(format digest with deep links)
        
        Main->>SK: post_slack(channel, text)
        SK->>Slack: slack_send_message()
        Slack-->>SK: OK
        SK-->>Main: Success
    else No changes
        alt Configured
            Main->>SK: post_slack(channel, info msg)
            SK->>Slack: slack_send_message()
        end
    end
    
    Main->>Snap: save_snapshot()
    Snap-->>Main: OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • sk_connectors.py: Scalekit SDK integration with OAuth, retry logic with configurable backoff, error handling for transient failures, and connector name inference require careful validation of error conditions and retry boundaries.
  • sf_customer_360.py: Multi-step orchestration with SOQL execution, snapshot-based delta detection, optional custom hook invocation, redaction logic, and Slack message formatting; integration points and fallback strategies need thorough tracing.
  • settings.py: Environment variable parsing and validation; ensure required field checks are exhaustive and error messaging is clear.
  • Integration points: Verify Scalekit tool names, parameter shapes, and error handling paths align across modules.

Possibly related PRs

  • blogops-app-examples#19: Introduces near-identical Scalekit integration (ScalekitConnector, get_connector singleton) and Settings class patterns, suggesting potential for cross-project alignment or code sharing.

Poem

🐰 A hop through Salesforce dreams so grand,
Where snapshots hold the promised land,
Deep links dance to Slack so blue,
Customer 360—nothing new!
Redact, digest, and post with care, 🔗✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.90% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Salesforce Digest Agent" directly describes the primary purpose of all changes in the changeset. The PR introduces a complete, self-contained system for orchestrating Salesforce data retrieval and posting digests to Slack, including configuration, utilities, connectors, and the main orchestration module. The title is concise, clear, and avoids vague terms or noise, making it immediately understandable to someone reviewing the project history.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (16)
salesforce-backed-customer-insight-agent/requirements.txt (1)

1-4: Consider version pinning strategy for production deployments.

The minimum version constraints (>=) allow automatic updates that could introduce breaking changes. For production environments, consider using exact pinning or compatible release constraints (e.g., ~=1.0.0) to ensure reproducible builds.

salesforce-backed-customer-insight-agent/README.md (2)

44-44: Consider formatting the bare URL as a markdown link.

The bare URL on Line 44 could be formatted as a proper markdown link for better readability: [https://hey.scalekit.dev](https://hey.scalekit.dev).


69-77: Consider adding language specifiers to fenced code blocks.

The fenced code blocks on lines 69 and 81 would benefit from language specifiers (e.g., text` or plaintext`) for better syntax highlighting and rendering.

Also applies to: 81-91

salesforce-backed-customer-insight-agent/slack_blocks.py (1)

3-4: Consider removing or documenting commented code.

The commented functions header_block and context_elems appear unused. If they're planned for future use, consider adding a comment explaining this; otherwise, remove them to reduce clutter.

Also applies to: 9-10

salesforce-backed-customer-insight-agent/sf_utils.py (3)

20-22: Consider moving the timedelta import to module level.

Importing timedelta inside the timedelta_hours function is unusual. Consider moving it to the top-level imports for consistency and slightly better performance.

Apply this diff:

-from datetime import datetime, timezone
+from datetime import datetime, timezone, timedelta

 # ... 

 def timedelta_hours(hours: int):
-    from datetime import timedelta
     return timedelta(hours=hours)

45-55: Consider logging exceptions for better observability.

The silent exception handling on lines 53-54 could hide important errors like permission issues, corrupted JSON, or disk problems. Adding basic logging would aid debugging without changing the graceful degradation behavior.

Apply this diff to add minimal logging:

+import logging
+
 # ... existing code ...

 def load_snapshot(path: str) -> Dict[str, str]:
     try:
         p = Path(path)
         if not p.exists(): return {}
         with p.open("r", encoding="utf-8") as f:
             data = json.load(f)
         if isinstance(data, dict):
             return {str(k): str(v) for k,v in data.items()}
-    except Exception:
+    except Exception as e:
+        logging.warning(f"Failed to load snapshot from {path}: {e}")
         pass
     return {}

57-64: Consider logging exceptions for better observability.

Similar to load_snapshot, the silent exception handling on lines 63-64 could hide important errors like permission issues, disk space problems, or path errors. Adding basic logging would aid debugging.

Apply this diff to add minimal logging:

 def save_snapshot(path: str, snapshot: Dict[str,str]) -> None:
     try:
         p = Path(path)
         p.parent.mkdir(parents=True, exist_ok=True)
         with p.open("w", encoding="utf-8") as f:
             json.dump(snapshot, f, ensure_ascii=False, indent=2)
-    except Exception:
+    except Exception as e:
+        logging.warning(f"Failed to save snapshot to {path}: {e}")
         pass
salesforce-backed-customer-insight-agent/sk_connectors.py (4)

116-160: Harden retry/backoff: add jitter and cap; prefer status codes when available.

Reduce synchronized retries and exponential blow‑ups; if ScalekitException exposes status codes, use them before message parsing.

@@
-        attempts = max(1, max_attempts or getattr(Settings, "RETRY_ATTEMPTS", 3))
-        backoff = max(1, getattr(Settings, "RETRY_BACKOFF_SECONDS", 1))
+        import random
+        attempts = max(1, max_attempts or getattr(Settings, "RETRY_ATTEMPTS", 3))
+        backoff = max(1, getattr(Settings, "RETRY_BACKOFF_SECONDS", 1))
+        max_backoff = max(backoff, getattr(Settings, "RETRY_MAX_BACKOFF_SECONDS", 16))
@@
-            except ScalekitException as e:
-                msg = str(e)
-                retryable = any(k in msg.lower() for k in ["429", "rate", "timeout", "unavailable", "temporar", "connection"])
+            except ScalekitException as e:
+                msg = str(e)
+                code = getattr(e, "status_code", None) or getattr(e, "code", None)
+                retryable = (
+                    (isinstance(code, int) and code in (408, 423, 429, 500, 502, 503, 504))
+                    or any(k in msg.lower() for k in ["429", "rate", "timeout", "unavailable", "temporar", "connection"])
+                )
                 if i < attempts and retryable:
-                    print(f"⚠️  {tool} failed (attempt {i}): {msg}")
-                    print(f"   Retrying in {backoff}s...")
-                    time.sleep(backoff)
-                    backoff *= 2
+                    sleep_s = backoff + random.uniform(0, backoff/2)
+                    print(f"⚠️  {tool} failed (attempt {i}): {msg}")
+                    print(f"   Retrying in {sleep_s:.1f}s...")
+                    time.sleep(sleep_s)
+                    backoff = min(backoff * 2, max_backoff)
                     continue
                 print(f"❌ {tool} failed permanently: {msg}")
                 return None
             except Exception as e:
                 if i >= attempts:
                     print(f"❌ {tool} error: {e}")
                     return None
-                print(f"⚠️  {tool} error: {e} — retrying in {backoff}s")
-                time.sleep(backoff)
-                backoff *= 2
+                sleep_s = backoff + random.uniform(0, backoff/2)
+                print(f"⚠️  {tool} error: {e} — retrying in {sleep_s:.1f}s")
+                time.sleep(sleep_s)
+                backoff = min(backoff * 2, max_backoff)

166-174: Sanitize nested params and broader secret keys in _preview.

Current logic only masks top‑level keys; nested secrets can leak.

-    def _preview(self, params: Dict[str, Any]) -> Dict[str, Any]:
-        safe = dict(params or {})
-        for k in ("text", "body", "description"):
-            if k in safe and isinstance(safe[k], str) and len(safe[k]) > 160:
-                safe[k] = safe[k][:160] + "…"
-        for k in ("token", "authorization", "auth", "secret", "password"):
-            if k in safe:
-                safe[k] = "***"
-        return safe
+    def _preview(self, params: Dict[str, Any]) -> Dict[str, Any]:
+        safe = dict(params or {})
+        secret_keys = {"token", "authorization", "auth", "secret", "password", "api_key", "client_secret"}
+
+        def _scrub(obj):
+            if isinstance(obj, dict):
+                out = {}
+                for k, v in obj.items():
+                    lk = str(k).lower()
+                    if lk in secret_keys:
+                        out[k] = "***"
+                    elif lk in {"text", "body", "description"} and isinstance(v, str) and len(v) > 160:
+                        out[k] = v[:160] + "…"
+                    else:
+                        out[k] = _scrub(v)
+                return out
+            if isinstance(obj, list):
+                return [_scrub(x) for x in obj]
+            return obj
+
+        return _scrub(safe)

57-66: Avoid printing full user identifiers.

user_identifier may be an email or user ID. Redact in logs.

Consider:

  • logging only a hash/first 3 chars, e.g., uid = user_identifier[:3] + "***" in log messages.
  • or use a structured logger with a PII-safe field.

180-184: Singleton is not thread‑safe.

If multiple threads call get_connector() concurrently, you can create >1 instance.

+import threading
 _connector: Optional[ScalekitConnector] = None
+_connector_lock = threading.Lock()
 
 def get_connector() -> ScalekitConnector:
-    global _connector
-    if _connector is None:
-        _connector = ScalekitConnector()
+    global _connector
+    if _connector is None:
+        with _connector_lock:
+            if _connector is None:
+                _connector = ScalekitConnector()
     return _connector
salesforce-backed-customer-insight-agent/sf_customer_360.py (5)

308-322: Use Settings.MAX_RECORDS instead of hard‑coding limits.

Keeps behavior configurable and consistent.

-    accts = fetch_accounts(
+    accts = fetch_accounts(
         conn,
         Settings.SALESFORCE_IDENTIFIER,
         since_iso,
         until_iso,
-        limit=200,
+        limit=Settings.MAX_RECORDS,
     )
-    opps = fetch_opps(
+    opps = fetch_opps(
         conn,
         Settings.SALESFORCE_IDENTIFIER,
         since_iso,
         until_iso,
-        limit=200,
+        limit=Settings.MAX_RECORDS,
     )

342-355: Split one‑liners and semicolon statements (Ruff E701/E702).

Improves readability and passes linters.

-    def _lastmod(x): return x.get("LastModifiedDate") or ""
+    def _lastmod(x):
+        return x.get("LastModifiedDate") or ""
     for a in accts:
-        aid = a.get("Id"); lm = _lastmod(a)
-        if not aid: continue
+        aid = a.get("Id")
+        lm = _lastmod(a)
+        if not aid:
+            continue
         cur[f"A:{aid}"] = lm
         if prev.get(f"A:{aid}") != lm:
             changed_accts.append(a)
     for o in opps:
-        oid = o.get("Id"); lm = _lastmod(o)
-        if not oid: continue
+        oid = o.get("Id")
+        lm = _lastmod(o)
+        if not oid:
+            continue
         cur[f"O:{oid}"] = lm
         if prev.get(f"O:{oid}") != lm:
             changed_opps.append(o)

243-257: Docstring vs implementation order mismatch.

Either update the docstring or reorder calls; current code tries salesforce_query_soql first.

-    Try order:
-      1) salesforce_soql_execute with {"soql": "..."}
-      2) salesforce_soql_execute with {"query": "..."}   (fallback)
-      3) salesforce_query_soql with {"query": "..."}     (alternate tool)
+    Try order:
+      1) salesforce_query_soql with {"query": "..."}     (alternate tool)
+      2) salesforce_soql_execute with {"soql": "..."}
+      3) salesforce_soql_execute with {"query": "..."}

136-146: SOQL construction: clamp limits and prefer parameterized inputs if supported.

Values come from internal code, but adding a defensive clamp helps. If the SDK supports parameter binding, prefer that.

-def fetch_accounts(conn, identifier: str, since_iso: str, until_iso: str, limit: int) -> List[Dict[str, Any]]:
+def fetch_accounts(conn, identifier: str, since_iso: str, until_iso: str, limit: int) -> List[Dict[str, Any]]:
+    limit = max(1, min(limit, Settings.MAX_RECORDS))
@@
 LIMIT {limit}
 """
     return soql(conn, identifier, q)
@@
-def fetch_opps(conn, identifier: str, since_iso: str, until_iso: str, limit: int) -> List[Dict[str, Any]]:
+def fetch_opps(conn, identifier: str, since_iso: str, until_iso: str, limit: int) -> List[Dict[str, Any]]:
+    limit = max(1, min(limit, Settings.MAX_RECORDS))

If the Scalekit Salesforce tools support bind parameters for datetimes/limit, consider using them instead of string formatting.

Also applies to: 149-159


14-23: Operational: imports may break when running from repo root due to hyphenated package name.

from settings import Settings works only if cwd is the module directory. Consider renaming the folder to use underscores and adding __init__.py, or provide a small launcher that sets sys.path.

Also applies to: 397-398

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 279fc5d and e21cc02.

📒 Files selected for processing (10)
  • salesforce-backed-customer-insight-agent/.env.example (1 hunks)
  • salesforce-backed-customer-insight-agent/.gitignore (1 hunks)
  • salesforce-backed-customer-insight-agent/README.md (1 hunks)
  • salesforce-backed-customer-insight-agent/custom_summarizer.py (1 hunks)
  • salesforce-backed-customer-insight-agent/requirements.txt (1 hunks)
  • salesforce-backed-customer-insight-agent/settings.py (1 hunks)
  • salesforce-backed-customer-insight-agent/sf_customer_360.py (1 hunks)
  • salesforce-backed-customer-insight-agent/sf_utils.py (1 hunks)
  • salesforce-backed-customer-insight-agent/sk_connectors.py (1 hunks)
  • salesforce-backed-customer-insight-agent/slack_blocks.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
salesforce-backed-customer-insight-agent/sk_connectors.py (1)
salesforce-backed-customer-insight-agent/settings.py (1)
  • Settings (9-38)
salesforce-backed-customer-insight-agent/sf_customer_360.py (4)
salesforce-backed-customer-insight-agent/settings.py (1)
  • Settings (9-38)
salesforce-backed-customer-insight-agent/sk_connectors.py (4)
  • get_connector (180-184)
  • execute_action_with_retry (116-161)
  • is_service_connected (57-66)
  • get_authorization_url (84-111)
salesforce-backed-customer-insight-agent/sf_utils.py (3)
  • load_snapshot (45-55)
  • save_snapshot (57-64)
  • redact_pii (24-43)
salesforce-backed-customer-insight-agent/custom_summarizer.py (1)
  • summarize_digest (32-42)
🪛 dotenv-linter (4.0.0)
salesforce-backed-customer-insight-agent/.env.example

[warning] 3-3: [UnorderedKey] The SCALEKIT_CLIENT_ID key should go before the SCALEKIT_ENV_URL key

(UnorderedKey)


[warning] 4-4: [UnorderedKey] The SCALEKIT_CLIENT_SECRET key should go before the SCALEKIT_ENV_URL key

(UnorderedKey)


[warning] 29-29: [UnorderedKey] The REDACT_EMAILS key should go before the SF_SNAPSHOT_FILE key

(UnorderedKey)


[warning] 30-30: [UnorderedKey] The REDACT_PHONES key should go before the SF_SNAPSHOT_FILE key

(UnorderedKey)

🪛 markdownlint-cli2 (0.18.1)
salesforce-backed-customer-insight-agent/README.md

31-31: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


32-32: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


33-33: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


44-44: Bare URL used

(MD034, no-bare-urls)


69-69: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


81-81: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.1)
salesforce-backed-customer-insight-agent/slack_blocks.py

25-25: Multiple statements on one line (colon)

(E701)


26-26: Multiple statements on one line (colon)

(E701)


27-27: Multiple statements on one line (colon)

(E701)

salesforce-backed-customer-insight-agent/custom_summarizer.py

32-32: Unused function argument: accounts

(ARG001)


32-32: Unused function argument: opportunities

(ARG001)

salesforce-backed-customer-insight-agent/sk_connectors.py

29-29: Avoid specifying long messages outside the exception class

(TRY003)


81-81: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)


88-88: Abstract raise to an inner function

(TRY301)


88-88: Avoid specifying long messages outside the exception class

(TRY003)


95-95: Multiple statements on one line (colon)

(E701)


96-96: Multiple statements on one line (colon)

(E701)


97-97: Multiple statements on one line (colon)

(E701)


100-100: Multiple statements on one line (colon)

(E701)


101-101: Multiple statements on one line (colon)

(E701)


102-102: Multiple statements on one line (colon)

(E701)


108-108: Consider moving this statement to an else block

(TRY300)


109-109: Do not catch blind exception: Exception

(BLE001)


153-153: Do not catch blind exception: Exception

(BLE001)

salesforce-backed-customer-insight-agent/sf_utils.py

25-25: Multiple statements on one line (colon)

(E701)


48-48: Multiple statements on one line (colon)

(E701)


53-54: try-except-pass detected, consider logging the exception

(S110)


53-53: Do not catch blind exception: Exception

(BLE001)


63-64: try-except-pass detected, consider logging the exception

(S110)


63-63: Do not catch blind exception: Exception

(BLE001)

salesforce-backed-customer-insight-agent/settings.py

6-6: Multiple statements on one line (colon)

(E701)


43-43: Do not catch blind exception: Exception

(BLE001)

salesforce-backed-customer-insight-agent/sf_customer_360.py

20-20: Do not catch blind exception: Exception

(BLE001)


41-41: Consider moving this statement to an else block

(TRY300)


42-42: Do not catch blind exception: Exception

(BLE001)


60-61: try-except-continue detected, consider logging the exception

(S112)


60-60: Do not catch blind exception: Exception

(BLE001)


86-86: Do not catch blind exception: Exception

(BLE001)


98-98: Do not catch blind exception: Exception

(BLE001)


122-122: Do not catch blind exception: Exception

(BLE001)


140-146: Possible SQL injection vector through string-based query construction

(S608)


153-159: Possible SQL injection vector through string-based query construction

(S608)


167-167: Multiple statements on one line (colon)

(E701)


168-168: Multiple statements on one line (colon)

(E701)


169-169: Multiple statements on one line (colon)

(E701)


344-344: Multiple statements on one line (semicolon)

(E702)


345-345: Multiple statements on one line (colon)

(E701)


350-350: Multiple statements on one line (semicolon)

(E702)


351-351: Multiple statements on one line (colon)

(E701)


376-376: Do not catch blind exception: Exception

(BLE001)


394-394: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)

🔇 Additional comments (8)
salesforce-backed-customer-insight-agent/.gitignore (1)

1-21: LGTM!

The gitignore patterns appropriately cover secrets, runtime state, Python artifacts, virtual environments, and editor/OS files.

salesforce-backed-customer-insight-agent/.env.example (1)

1-30: LGTM!

The environment variable template is well-documented with clear comments explaining required vs. optional variables, defaults, and usage examples. The structure supports the application's configuration needs effectively.

salesforce-backed-customer-insight-agent/custom_summarizer.py (1)

32-42: LGTM!

The placeholder hook design is appropriate. The unused argument warnings from static analysis are expected for this pattern, as this function is meant to be customized by users who will use those parameters.

salesforce-backed-customer-insight-agent/settings.py (2)

5-7: LGTM!

The boolean parsing helper correctly handles None and various truthy string representations.


28-44: Validation logic is appropriate.

The required variable checking and module-level validation provide good guardrails. The broad exception catch on Line 43 is acceptable in this context for capturing any configuration errors during module import.

salesforce-backed-customer-insight-agent/slack_blocks.py (1)

6-28: LGTM!

The Slack block builder functions are well-structured with proper type hints and correctly construct Slack message blocks.

salesforce-backed-customer-insight-agent/sf_utils.py (2)

1-7: LGTM!

The imports and regex patterns for email and phone detection are appropriate.


24-43: LGTM!

The PII redaction logic appropriately masks emails (preserving domain visibility) and phone numbers (preserving last 2 digits) while handling edge cases for short values.

SLACK_IDENTIFIER = os.getenv("SLACK_IDENTIFIER")

DIGEST_CHANNEL_ID = os.getenv("DIGEST_CHANNEL_ID","")
LOOKBACK_HOURS = int(os.getenv("LOOKBACK_HOURS","24"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for integer conversions.

The int() conversions on lines 18 and 22 will raise ValueError at module import time if the environment variables contain non-numeric values. This will prevent the module from loading before Settings.validate() can provide a user-friendly error message.

Apply this diff to add defensive error handling:

-    LOOKBACK_HOURS = int(os.getenv("LOOKBACK_HOURS","24"))
+    try:
+        LOOKBACK_HOURS = int(os.getenv("LOOKBACK_HOURS","24"))
+    except ValueError:
+        LOOKBACK_HOURS = 24
+        print("⚠️ Invalid LOOKBACK_HOURS value, using default: 24")
-    MAX_RECORDS = int(os.getenv("MAX_RECORDS","200"))
+    try:
+        MAX_RECORDS = int(os.getenv("MAX_RECORDS","200"))
+    except ValueError:
+        MAX_RECORDS = 200
+        print("⚠️ Invalid MAX_RECORDS value, using default: 200")

Also applies to: 22-22

🤖 Prompt for AI Agents
In salesforce-backed-customer-insight-agent/settings.py around lines 18 and 22,
the direct int(os.getenv(...)) calls can raise ValueError at import time; wrap
each environment-to-int conversion in a safe parse that tries to convert the env
var to int and falls back to the provided default when conversion fails (or
returns None), and log or store the raw env value so Settings.validate() can
later produce a user-friendly error; implement this by adding a small helper
function (e.g., parse_env_int(key, default)) that catches ValueError/TypeError
and returns the default, then replace the two int(...) calls with calls to that
helper.

Comment on lines +281 to +289
def run_customer_360():
from datetime import timedelta
print("🚀 Starting Salesforce → Slack Customer 360 Insights")

# Connectors
conn = get_connector()

# Validate connections or emit OAuth URLs
if not conn.is_service_connected("salesforce", Settings.SALESFORCE_IDENTIFIER):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate configuration at startup.

Fail fast if required env vars are missing.

 def run_customer_360():
-    from datetime import timedelta
     print("🚀 Starting Salesforce → Slack Customer 360 Insights")
 
     # Connectors
     conn = get_connector()
 
+    # Ensure all required settings are present
+    try:
+        Settings.validate()
+    except ValueError as e:
+        print(f"❌ Configuration error: {e}")
+        return
🤖 Prompt for AI Agents
In salesforce-backed-customer-insight-agent/sf_customer_360.py around lines 281
to 289, add a startup configuration validation that checks required
environment-based Settings (e.g., Settings.SALESFORCE_IDENTIFIER and other
required SALESFORCE_* and SLACK_* settings) before creating connectors; if any
required setting is missing or empty, log an explicit error and exit/fail fast
(raise SystemExit or call sys.exit(1)) with a clear message so the process does
not continue with invalid configuration.

Comment on lines +380 to +396
if Settings.DIGEST_CHANNEL_ID:
txt = build_text_fallback(
domain,
changed_accts,
changed_opps,
lookback_hours=Settings.LOOKBACK_HOURS,
summary_text=summary_text,
)
res = post_slack(conn, Settings.SLACK_IDENTIFIER, Settings.DIGEST_CHANNEL_ID, text=txt, blocks=None)
ch = (res or {}).get("channel")
ts = (res or {}).get("ts") or (res or {}).get("timestamp")
meta = {"channel": ch, "ts": ts}
print("✅ Posted to Slack:", json.dumps(meta, indent=2))
else:
print("ℹ️ DIGEST_CHANNEL_ID not set; printing text message:")
print(txt)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: txt referenced before assignment when DIGEST_CHANNEL_ID is not set.

Compute txt before the branch.

-    # Post (text-only). Text includes a compact summary with deep links.
-    if Settings.DIGEST_CHANNEL_ID:
-        txt = build_text_fallback(
-            domain,
-            changed_accts,
-            changed_opps,
-            lookback_hours=Settings.LOOKBACK_HOURS,
-            summary_text=summary_text,
-        )
-        res = post_slack(conn, Settings.SLACK_IDENTIFIER, Settings.DIGEST_CHANNEL_ID, text=txt, blocks=None)
+    # Post (text-only). Text includes a compact summary with deep links.
+    txt = build_text_fallback(
+        domain,
+        changed_accts,
+        changed_opps,
+        lookback_hours=Settings.LOOKBACK_HOURS,
+        summary_text=summary_text,
+    )
+    if Settings.DIGEST_CHANNEL_ID:
+        res = post_slack(conn, Settings.SLACK_IDENTIFIER, Settings.DIGEST_CHANNEL_ID, text=txt, blocks=None)
         ch = (res or {}).get("channel")
         ts = (res or {}).get("ts") or (res or {}).get("timestamp")
         meta = {"channel": ch, "ts": ts}
         print("✅ Posted to Slack:", json.dumps(meta, indent=2))
     else:
         print("ℹ️ DIGEST_CHANNEL_ID not set; printing text message:")
         print(txt)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.1)

394-394: String contains ambiguous (INFORMATION SOURCE). Did you mean i (LATIN SMALL LETTER I)?

(RUF001)

🤖 Prompt for AI Agents
In salesforce-backed-customer-insight-agent/sf_customer_360.py around lines 380
to 396, txt is only created inside the DIGEST_CHANNEL_ID branch which causes an
unbound reference in the else path; move the call to build_text_fallback above
the if so txt is computed regardless of DIGEST_CHANNEL_ID (keep the same
arguments: domain, changed_accts, changed_opps,
lookback_hours=Settings.LOOKBACK_HOURS, summary_text=summary_text), then use
that txt in the if branch to post to Slack and in the else branch to print it.

Comment on lines +27 to +35
def __init__(self) -> None:
if not Settings.SCALEKIT_CLIENT_ID or not Settings.SCALEKIT_CLIENT_SECRET:
raise ValueError("Scalekit credentials not configured")

self.client = ScalekitClient(
env_url=Settings.SCALEKIT_ENV_URL,
client_id=Settings.SCALEKIT_CLIENT_ID,
client_secret=Settings.SCALEKIT_CLIENT_SECRET,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate config before constructing the client.

Initialize only after asserting all required settings; also guard against empty env_url.

 class ScalekitConnector:
     def __init__(self) -> None:
-        if not Settings.SCALEKIT_CLIENT_ID or not Settings.SCALEKIT_CLIENT_SECRET:
-            raise ValueError("Scalekit credentials not configured")
+        # Fail fast on missing config
+        Settings.validate()
+        if not Settings.SCALEKIT_ENV_URL:
+            raise ValueError("Scalekit env URL not configured")
 
         self.client = ScalekitClient(
             env_url=Settings.SCALEKIT_ENV_URL,
             client_id=Settings.SCALEKIT_CLIENT_ID,
             client_secret=Settings.SCALEKIT_CLIENT_SECRET,
         )

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.1)

29-29: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In salesforce-backed-customer-insight-agent/sk_connectors.py around lines 27 to
35, validate all required settings before constructing ScalekitClient: check
SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET and SCALEKIT_ENV_URL for
presence/non-empty (strip whitespace) and if any are missing raise a ValueError
that lists which settings are missing, then only after validation instantiate
ScalekitClient using the validated values.

Comment on lines +84 to +112
def get_authorization_url(self, service: str, user_identifier: str) -> str:
try:
connector_name = self._guess_connector_name(service, user_identifier)
if not connector_name:
raise RuntimeError(f"No connector available for service '{service}'")
resp = self.client.actions.connected_accounts.get_magic_link_for_connected_account(
connector=connector_name,
identifier=user_identifier,
)
# Normalize link from different SDK shapes
candidates = []
if hasattr(resp, "link"): candidates.append(resp.link)
if hasattr(resp, "magic_link"): candidates.append(resp.magic_link)
if hasattr(resp, "authorization_link"): candidates.append(resp.authorization_link)
if isinstance(resp, (list, tuple)) and resp:
obj = resp[0]
if hasattr(obj, "link"): candidates.append(obj.link)
if hasattr(obj, "magic_link"): candidates.append(obj.magic_link)
if hasattr(obj, "authorization_link"): candidates.append(obj.authorization_link)
for url in candidates:
if url:
print(f"🔗 OAuth link for {service}: {url}")
return url
print(f"⚠️ Unexpected magic-link response for {service}: {resp}")
return ""
except Exception as e:
print(f"❌ Error generating OAuth link for {service}/{user_identifier}: {e}")
return ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t log full OAuth magic links; narrow exceptions and fix one‑liners.

Avoid leaking authorization URLs in logs, prefer redacted/host-only. Replace E701 one‑liners and catch ScalekitException explicitly.

     def get_authorization_url(self, service: str, user_identifier: str) -> str:
-        try:
+        try:
             connector_name = self._guess_connector_name(service, user_identifier)
             if not connector_name:
                 raise RuntimeError(f"No connector available for service '{service}'")
             resp = self.client.actions.connected_accounts.get_magic_link_for_connected_account(
                 connector=connector_name,
                 identifier=user_identifier,
             )
             # Normalize link from different SDK shapes
-            candidates = []
-            if hasattr(resp, "link"): candidates.append(resp.link)
-            if hasattr(resp, "magic_link"): candidates.append(resp.magic_link)
-            if hasattr(resp, "authorization_link"): candidates.append(resp.authorization_link)
-            if isinstance(resp, (list, tuple)) and resp:
-                obj = resp[0]
-                if hasattr(obj, "link"): candidates.append(obj.link)
-                if hasattr(obj, "magic_link"): candidates.append(obj.magic_link)
-                if hasattr(obj, "authorization_link"): candidates.append(obj.authorization_link)
+            candidates: list[str] = []
+            if hasattr(resp, "link"):
+                candidates.append(resp.link)                # type: ignore[attr-defined]
+            if hasattr(resp, "magic_link"):
+                candidates.append(resp.magic_link)         # type: ignore[attr-defined]
+            if hasattr(resp, "authorization_link"):
+                candidates.append(resp.authorization_link) # type: ignore[attr-defined]
+            if isinstance(resp, (list, tuple)) and resp:
+                obj = resp[0]
+                if hasattr(obj, "link"):
+                    candidates.append(obj.link)            # type: ignore[attr-defined]
+                if hasattr(obj, "magic_link"):
+                    candidates.append(obj.magic_link)      # type: ignore[attr-defined]
+                if hasattr(obj, "authorization_link"):
+                    candidates.append(obj.authorization_link) # type: ignore[attr-defined]
             for url in candidates:
                 if url:
-                    print(f"🔗 OAuth link for {service}: {url}")
+                    # Log only host/path; avoid leaking tokens
+                    try:
+                        from urllib.parse import urlparse
+                        u = urlparse(url)
+                        redacted = f"{u.scheme}://{u.netloc}{u.path}"
+                    except Exception:
+                        redacted = "<redacted>"
+                    print(f"🔗 OAuth link generated for {service}: {redacted}")
                     return url
             print(f"⚠️  Unexpected magic-link response for {service}: {resp}")
             return ""
-        except Exception as e:
+        except ScalekitException as e:
+            print(f"❌ Error generating OAuth link for {service}/{user_identifier}: {e}")
+            return ""
+        except Exception as e:
             print(f"❌ Error generating OAuth link for {service}/{user_identifier}: {e}")
             return ""

Also applies to: 95-103

🧰 Tools
🪛 Ruff (0.14.1)

88-88: Abstract raise to an inner function

(TRY301)


88-88: Avoid specifying long messages outside the exception class

(TRY003)


95-95: Multiple statements on one line (colon)

(E701)


96-96: Multiple statements on one line (colon)

(E701)


97-97: Multiple statements on one line (colon)

(E701)


100-100: Multiple statements on one line (colon)

(E701)


101-101: Multiple statements on one line (colon)

(E701)


102-102: Multiple statements on one line (colon)

(E701)


108-108: Consider moving this statement to an else block

(TRY300)


109-109: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In salesforce-backed-customer-insight-agent/sk_connectors.py around lines 84-112
(and similarly 95-103), the method currently prints full OAuth magic links, uses
one-line if statements (E701) and catches broad Exception only; change it to
explicitly catch ScalekitException first then a generic Exception, replace
one-line ifs with proper multi-line blocks, avoid printing full authorization
URLs by redacting to host-only (use urllib.parse to extract and log only
scheme://hostname or a short masked form), and when logging unexpected responses
or errors avoid dumping full resp contents—log a concise, redacted summary or
type instead. Ensure return behavior stays the same (return the first non-empty
link or empty string) while removing sensitive data from logs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant