presidio-v/presidio-hardened-angellist

GitHub: presidio-v/presidio-hardened-angellist

一款安全加固的早期初创公司交易流筛选与尽职调查工具,从 AngelList 融资邮件中自动提取结构化信息并进行评分定级。

Stars: 5 | Forks: 0

# presidio-hardened-angellist [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/presidio-v/presidio-hardened-angellist/actions/workflows/ci.yml) [![CodeQL](https://static.pigsec.cn/wp-content/uploads/repos/cas/53/539e9a6bf48ad24469a4363bff3aa68124154549e26592783d3d8577f2acbbfc.svg)](https://github.com/presidio-v/presidio-hardened-angellist/actions/workflows/codeql.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) Presidio 安全加固的**交易流筛选与尽职调查工具包**,专为 通过 **AngelList 投资联盟**寻找的早期(pre-seed / seed)初创公司设计。 ## 工作原理 ``` forwarded .eml ─▶ intake ─▶ extraction ─▶ enrichment ─▶ triage rubric ─▶ memo (parse) (regex first, (hardened (deterministic (Claude or LLM fallback) HTTP fetch) scorecard) template) ``` 1. **收集** — 将转发的 `.eml`(或粘贴的文本)解析为结构化的 `Deal`。 2. **提取** — 优先使用确定性的 regex/heuristics;仅在解析信息过少 (`is_complete()` 为 `False`)时启用 **Claude fallback**。 3. **补充** *(可选)* — 通过加固的 session 获取公司网站,以回填一句话简介。 4. **筛选** — 根据确定性的 pre-seed/seed 标准进行评分 → 综合得分 + 等级(`Pass` / `Track` / `Dig deeper` / `Strong lead`)。 5. **备忘录** *(可选)* — Claude 辅助的投资备忘录,带有模板化 fallback,因此即使没有 API key,`--memo` 依然有效。 确定性路径**无需 API key**。LLM 步骤仅在 设置了 `ANTHROPIC_API_KEY` 且安装了 `[llm]` 扩展时才会激活。 ## 安装说明 ``` pip install presidio-hardened-angellist # deterministic core pip install 'presidio-hardened-angellist[llm]' # + Claude extraction/memo ``` 用于开发: ``` git clone https://github.com/presidio-v/presidio-hardened-angellist.git cd presidio-hardened-angellist uv venv && source .venv/bin/activate uv pip install -e ".[dev,llm]" ``` ## CLI 用法 ``` angeltriage deal.eml # scorecard for one deal angeltriage deal.eml --memo # + investment memo angeltriage deal.eml --enrich # fetch the company site for more signal angeltriage deal.eml --json # machine-readable output (pipe-friendly) cat deal.txt | angeltriage - # read a pasted email from stdin angeltriage *.eml # batch, ranked by composite score angeltriage deals.csv # batch-triage a CSV of deals (one row each) angeltriage --imap # pull deal emails over IMAP (see below) angeltriage --watch --interval 300 # poll IMAP every 5 min, auto-triage new deals angeltriage deal.eml --no-llm # force the deterministic-only path angeltriage deal.eml --weights w.json # tune dimension weights (see below) angeltriage deal.eml --rubric r.json # full rubric config (see below) angeltriage deal.eml --save # persist to the deal queue (see below) angeltriage --queue # show the ranked, saved deal queue angeltriage --set-status 4 passed # update a saved deal's workflow status ``` `.eml`/文本输入会被作为电子邮件解析;`.csv` 输入会每次一行地进行筛选。 您可以在一个批次中混合文件 —— 所有内容都会按得分一起排名。 示例输出: ``` Nimbus Robotics [Strong lead · 83.0/100] pre-seed · SAFE · $10,000,000 cap · lead: Jane Okafor Warehouse-automation robots for SMB 3PLs. Scorecard: Team 4.5/5 2 founders; credential signals: ex-, former, mit Market 3.5/5 clear one-liner present Traction 4.5/5 signals: customers, month-over-month, mrr, paying Terms 4.0/5 cap $10,000,000; SAFE Syndicate 4.0/5 lead: Jane Okafor; allocation $250,000 ``` ## 库用法 ``` from presidio_angellist import triage_email result = triage_email("deal.eml", memo=True) print(result.scorecard.tier, result.scorecard.composite) # Strong lead 83.0 print(result.deal.valuation_cap) # 10000000.0 print(result.memo) ``` 调整标准权重: ``` from presidio_angellist import score_deal, parse_email deal = parse_email("deal.eml") sc = score_deal( deal, weights={"team": 0.4, "market": 0.2, "traction": 0.2, "terms": 0.1, "syndicate": 0.1} ) ``` ## 筛选标准 (pre-seed / seed) | 维度 | 权重考量内容 | |---|---| | **Team** | 创始人数量、技术联合创始人、资历信号(ex-FAANG、YC 等) | | **Market** | 一句话简介的清晰度 / 行业定位 | | **Traction** | 营收、用户数、LOIs、增长率 —— 任何早期信号 | | **Terms** | 针对该阶段的估值上限合理性、金融工具(SAFE/定价) | | **Syndicate** | 具名的领投人、分配额度、社会背书 | 风险标记(独立创始人、缺失估值上限、阶段估值上限过高、无业务进展、 无网站)会单独标出。 ### 超出范围(成长期)检测 该标准主要针对 **pre-seed / seed**。当一笔交易看起来处于较后阶段 —— 明确的 Series A/B/C、ARR/revenue ≥ $5M,或者规模庞大的定价/风投轮次 —— 它将被标记为 **`Out of scope`** 并附注(例如 *"Likely growth-stage (~$40M ARR; venture round $20M) — outside pre-seed/seed scope; score is indicative only"*),而不是给予一个误导性的评级。综合得分 仍会计算,但仅作为指示性参考。该功能通过 `detect_stage_scope(deal)` 和 `Scorecard.scope_note` 暴露。 ### 调整权重 权重位于 `DEFAULT_WEIGHTS` 中,可在每次调用时覆盖,或通过 `--weights` 从 JSON 配置文件覆盖: ``` { "team": 0.5, "traction": 0.3 } ``` ``` angeltriage deal.eml --weights weights.json ``` 未指定的维度将保留其默认权重(因此部分覆盖也是可以的), 权重之和无需为 1(综合得分会根据总权重进行归一化), 并且至少有一个权重必须为正数。有效的维度包括:`team`、`market`、`traction`、 `terms`、`syndicate`。通过库调用: ``` from presidio_angellist import load_weights, triage_email result = triage_email("deal.eml", weights=load_weights("weights.json")) ``` ### 完整的标准配置 (`--rubric`) 如果需要配置除权重外的更多内容,请传入 `--rubric` 文件。 所有部分均为可选,并与默认值合并: ``` { "weights": { "team": 0.4, "traction": 0.25 }, "tier_thresholds": { "Strong lead": 90, "Dig deeper": 75 }, "cap_ceilings": { "pre-seed": 8000000, "seed": 25000000 }, "risk_penalty": 5.0 } ``` - **`tier_thresholds`** — 每个等级标签的最低综合得分 (0–100)。 `Pass` 的最低值始终保持在 0。 - **`cap_ceilings`** — 各阶段的估值上限 (USD);超过该上限将触发 风险标记并扣除 Terms 得分。 - **`risk_penalty`** — **每个风险标记**扣除的综合得分 (默认 0)。 ``` angeltriage deal.eml --rubric rubric.json # mutually exclusive with --weights ``` ``` from presidio_angellist import load_rubric_config, triage_email result = triage_email("deal.eml", config=load_rubric_config("rubric.json")) ``` 验证采取闭环失败原则 —— 未知的键/维度、超出范围的阈值、 负数惩罚或格式错误的 JSON 会引发 `WeightsConfigError`。 ### CSV 批量导入 `angeltriage deals.csv` 会逐行筛选 `Deal`。系统会不区分大小写地匹配 常见的列头别名: | 字段 | 接受的表头(任意一个) | |---|---| | company | `company`, `name`, `startup` | | valuation_cap | `valuation_cap`, `cap`, `valuation` | | round_size | `round_size`, `raising`, `round`, `target` | | website | `website`, `url`, `site` | | founders | `founders`, `founder`, `team` (以 `;` / `,` 分隔) | | … | `one_liner`, `sector`, `stage`, `instrument`, `allocation`, `lead`, `deadline`, `location`, `traction`, `links` | 金额单元格接受 `$1.2M`, `1,200,000` 或 `500k`。没有公司名称的行将被 跳过。 ``` from presidio_angellist import triage_csv for result in triage_csv("deals.csv"): print(result.deal.company, result.scorecard.tier) ``` ## IMAP 获取 `--imap` 直接从邮箱拉取交易邮件(将投资联盟邮件归档到 一个文件夹中,然后进行轮询)。它在**任何您运行它的地方**运行 —— 您的笔记本电脑或服务器, 而不是手机。凭证**仅**来自环境变量(绝不通过命令 行)—— 请使用**应用专用密码**(iCloud,开启 2FA 的 Gmail): ``` export IMAP_HOST=imap.mail.me.com # iCloud; Gmail: imap.gmail.com export IMAP_USER=you@icloud.com export IMAP_PASSWORD=abcd-efgh-ijkl-mnop # app-specific password export IMAP_FOLDER=Deals # optional; defaults to INBOX angeltriage --imap --save # fetch UNSEEN, triage, save to the queue angeltriage --imap --imap-all --imap-limit 20 # most recent 20, read or not angeltriage --imap --imap-from deals@syndicate.com ``` | 环境变量 | 用途 | |---|---| | `IMAP_HOST` / `IMAP_USER` / `IMAP_PASSWORD` | 必需的连接 + 应用专用密码 | | `IMAP_PORT` | 可选,默认 `993` | | `IMAP_FOLDER` | 可选,默认 `INBOX`(或使用 `--imap-folder`) | | `IMAP_SSL` | 可选,默认开启(`0`/`false` 可禁用) | Flags:`--imap-folder`,`--imap-all`(不仅是 `UNSEEN`),`--imap-from ADDR`, `--imap-limit N`。邮箱以**只读**模式打开,因此邮件不会被标记为 已读 —— 重新轮询时会重新获取它们,并且交易队列会根据交易标识去重。 ``` from presidio_angellist import imap_config_from_env, triage_imap cfg = imap_config_from_env(folder="Deals", limit=20) # reads IMAP_* env vars for result in triage_imap(cfg): print(result.deal.company, result.scorecard.tier) ``` ### 监视模式(连续轮询) `--watch` 会按间隔轮询邮箱,并自动将新交易筛选进 队列 —— 一个无需人工干预的收件箱到队列 pipeline: ``` angeltriage --watch --interval 300 # poll every 5 min until Ctrl-C, saving new deals angeltriage --watch --max-cycles 12 # poll 12 times then stop ``` 在同一个会话中,邮件会根据 `Message-ID` 去重,因此同一封未读邮件不会 在每次轮询时被重新筛选;在重启过程中,交易队列会根据交易 标识去重。首次轮询遇到配置/凭证错误时会快速失败;后续轮询 会容忍瞬时的网络错误并继续运行。每次轮询都会打印一行 摘要以及任何新保存的交易。对于 cron 式设置,请定期使用 `--max-cycles 1` 代替长时间运行的进程。 ``` from presidio_angellist import DealStore, imap_config_from_env, watch with DealStore() as store: watch(imap_config_from_env(folder="Deals"), store, interval=300) ``` ## 本地 / 自托管 LLM 默认情况下,LLM 层使用 Anthropic (`ANTHROPIC_API_KEY`)。如果需要运行 **本地或自托管**的 OpenAI 兼容模型(如 mlx_lm.server, Ollama, vLLM, LM Studio 等),请设置 base URL —— 仅此一项即可切换后端: ``` export ANGELTRIAGE_LLM_BASE_URL=http://127.0.0.1:8080/v1 export ANGELTRIAGE_LLM_MODEL=my-local-model export ANGELTRIAGE_LLM_API_KEY=not-needed # optional; many local servers ignore it export ANGELTRIAGE_LLM_TIMEOUT=120 # optional, seconds ``` | 环境变量 | 用途 | |---|---| | `ANGELTRIAGE_LLM_BASE_URL` | OpenAI 兼容的 base URL(例如 `…/v1`);**设置此项即可使用本地模型** | | `ANGELTRIAGE_LLM_MODEL` | 服务器期望的模型 ID | | `ANGELTRIAGE_LLM_API_KEY` | 可选的 bearer token (默认 `not-needed`) | | `ANGELTRIAGE_LLM_PROVIDER` | 可选,强制指定 `openai`/`anthropic` 后端 | | `ANGELTRIAGE_LLM_EXTRA_BODY` | 可选的 JSON,会合并到请求体中,用于设置服务器特定参数 | 对于**推理模型**(例如 Qwen3),请禁用其思考过程,以便直接返回 最终答案(否则它可能只输出推理 token 而无实际内容): ``` export ANGELTRIAGE_LLM_EXTRA_BODY='{"chat_template_kwargs":{"enable_thinking":false}}' ``` 本地端点通常是回环地址,这会被补充功能的 SSRF 防护拦截 —— 因此 LLM 调用会故意绕过该防护。仅将其指向您控制的 服务器 ([SECURITY.md](SECURITY.md))。如果模型不可访问,筛选将降级为 确定性评分 + 模板化备忘录,而不是直接报错失败。 ## 邮件通知 (`--notify`) `--notify` 会通过 SMTP 将**新加入存储**的交易邮件发送给收件人列表 —— 非常适合每日无人值守的运行。配置仅通过环境变量进行: ``` export ANGELTRIAGE_SMTP_HOST=smtp.example.com export ANGELTRIAGE_SMTP_PORT=465 # 465 = implicit TLS; else STARTTLS export ANGELTRIAGE_SMTP_USER=you@example.com export ANGELTRIAGE_SMTP_PASSWORD=… # app-specific password export ANGELTRIAGE_SMTP_FROM=you@example.com # optional, defaults to USER export ANGELTRIAGE_NOTIFY_TO="a@example.com, b@example.com" # 每日 one-shot: 轮询 al folder 一次,进行 triage、保存,并通过 email 发送新 deals angeltriage --watch --max-cycles 1 --imap-folder al --notify ``` `--notify` 需要一个存储(在 `--watch` 下隐式开启;否则需传递 `--save`),并且 只会发送真正全新的交易邮件 —— 因此只读邮箱重新获取时不会重发。 结合 `--watch --max-cycles 1`,持久化的 `processed_messages` 表可确保每封 邮件在每日运行中被筛选**且仅筛选一次**。发送失败会抛出明显错误 (非零退出码),因此交易绝不会悄无声息地被丢弃。 ## 交易队列(持久化) `--save` 将筛选后的交易持久化到本地 SQLite 存储中,使筛选成为 一个您可以长期跟进的工作流,而不是一次性操作: ``` angeltriage inbox/*.eml --save # triage + save the batch angeltriage --queue # ranked list of everything saved angeltriage --queue --status new # filter by workflow status angeltriage --set-status 4 tracking # new -> tracking -> passed -> committed ``` ``` # tier score status seen company 1 Strong lead 83.0 tracking 2 Nimbus Robotics 3 Track 49.5 new 1 Solo Stealth ``` - **跨运行去重** — 交易以网站域名(或者在无网站时使用规范化的 公司名称)作为键,因此由两个 投资联盟转发的同一笔交易会合并为一行。`seen` 会记录其到达的次数。 - **重新保存时保留状态** — 重新筛选一笔 `passed` 的交易不会将其重置 为 `new`;只会刷新记分卡/得分。 - **存储位置** — 默认为 `~/.angeltriage/deals.db`;使用 `--db FILE` 或 `ANGELTRIAGE_DB` 环境变量覆盖。数据库在本地;数据不会离开您的 机器。 ``` from presidio_angellist import DealStore, triage_email with DealStore() as store: # default path, or DealStore("deals.db") saved, is_new = store.save(triage_email("deal.eml")) for row in store.list(status="new"): print(row.id, row.company, row.tier, row.composite) store.set_status(saved.id, "tracking") ``` ## 安全加固(保留,并用于数据补充) | 特性 | 功能 | |---|---| | **强制严格的 TLS 1.2+** | 拒绝 TLS 1.0/1.1;仅使用临时 EC 密码;始终开启 `verify=True` | | **HTTP → HTTPS 自动升级** | 静默升级不安全的 `http://` URL;拒绝非 HTTP(S) 协议 | | **SSRF 防护** | 拒绝解析为回环/私有/链路本地(包括 `169.254.169.254`)/保留地址的目标 | | **API key / 密钥脱敏** | `presidio_angellist` logger 上的 `RedactingFilter` 会从**每一条**日志记录中清除 Bearer token、`sk_live_*` / `sk-ant-*` 密钥以及 `access_token=`/`api_key=` | | **带退避的重试机制** | 在遇到连接错误 / 429 / 5xx 时进行指数退避,并遵循 `Retry-After` | | **基于主机的速率限制** | Token-bucket 限制器;防止对数据补充目标主机造成意外 DoS | | **安全事件日志记录** | 对每一项加固操作进行结构化日志记录(`presidio_angellist` logger) | 每一个向外的数据补充请求都会经过 `HardenedSession`。发送到 可选 LLM 层的不可信交易文本会被隔离,且 system prompt 会将其视为数据, 而非指令(防止 prompt injection);除非明确选择启用,否则拒绝明文 IMAP。 有关完整的信任边界模型,请参阅 [SECURITY.md](SECURITY.md)。 ## 路线图 | 版本 | 亮点 | |---|---| | **0.2.** | 转向交易流筛选:邮件获取、确定性标准、`--weights` 配置、LLM 提取 fallback + 备忘录、`angeltriage` CLI | | **0.3.0** | CSV/批量导入、完整标准配置(`--rubric`:等级、估值上限、单标记惩罚)、HTML 邮件健壮性、og/title 补充 fallback | | **0.4.0** | SQLite 交易队列:`--save` / `--queue` / `--set-status`、跨运行去重、工作流状态 | | **0.5.0** | IMAP 获取(`--imap`,密钥校验) | | **0.5.1** | IMAP watch 模式(`--watch`:间隔轮询、会话内去重、自动保存) | | **0.5.2** | 更好的公司/一句话简介提取(正文线索);成长期超范围检测 | | **0.6.0** | 安全加固版本:SSRF 防护、sink 强制日志脱敏、LLM prompt injection 防御、恢复重试/退避、拒绝明文 IMAP、基于 CVE 的依赖项 + CI 中的 `pip-audit` | | **0.7.0** | 本地/自托管 LLM 后端(OpenAI 兼容)、SMTP 交易通知(`--notify`)、恰好一次轮询(`processed_messages`) | | **0.8.0** _(计划中)_ | 可插拔的数据补充提供商(Crunchbase/Harmonic)、队列导出/摘要 | ## 运行测试 ``` pytest -v --cov=presidio_angellist --cov-report=term-missing ``` ## 项目结构 ``` presidio-hardened-angellist/ ├── src/presidio_angellist/ │ ├── __init__.py # public API │ ├── hardening.py # TLS / redaction / rate-limit primitives │ ├── models.py # Deal, Scorecard, TriageResult │ ├── intake/email.py # forwarded .eml / text -> Deal (deterministic) │ ├── intake/csv.py # CSV of deals -> list[Deal] │ ├── intake/imap.py # pull deal emails over IMAP (key-gated) │ ├── watch.py # --watch: poll IMAP on an interval, auto-triage │ ├── enrich/web.py # hardened website enrichment │ ├── rubric_config.py # RubricConfig + defaults (weights/tiers/ceilings) │ ├── triage/rubric.py # deterministic pre-seed/seed scorecard │ ├── triage/memo.py # LLM memo + templated fallback │ ├── store.py # SQLite-backed persistent deal queue │ ├── config.py # --weights / --rubric config loaders │ ├── llm.py # optional Claude extraction/memo (key-gated) │ ├── pipeline.py # end-to-end triage_email() │ └── cli.py # angeltriage entrypoint ├── tests/ ├── pyproject.toml ├── LICENSE # MIT ├── README.md └── SECURITY.md ``` ## 许可证 MIT — 详见 [LICENSE](./LICENSE)。 ## 安全 有关我们的漏洞披露政策,请参阅 [SECURITY.md](./SECURITY.md)。 ## SDLC 本仓库在 Presidio 加固家族的 SDLC 下开发: .
标签:LLM集成, Python, 投资分析, 无后门, 逆向工具, 金融科技