diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2724a1e..26da468 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,13 +6,13 @@ }, "metadata": { "description": "Official Extruct AI skills for company lookup, discovery, Deep Search, AI tables, enrichment, and people workflows", - "version": "1.0.6", + "version": "1.0.7", "repository": "https://github.com/extruct-ai/skills" }, "plugins": [ { "name": "extruct-skills", - "version": "1.0.6", + "version": "1.0.7", "description": "Official Extruct AI skill bundle for company lookup, semantic search, lookalike search, Deep Search, table operations, enrichment, and contact-finding workflows.", "source": "./", "strict": true, diff --git a/skills/extruct-api/SKILL.md b/skills/extruct-api/SKILL.md index 9c91df3..85620e6 100644 --- a/skills/extruct-api/SKILL.md +++ b/skills/extruct-api/SKILL.md @@ -1,6 +1,6 @@ --- name: extruct-api -description: Run explicit Extruct API tasks through the bundled Extruct CLI. Covers Deep Search, semantic search, lookalike search, company and people tables, column operations, enrichment, and contact finding. +description: Run explicit Extruct API tasks through the bundled Extruct CLI. Covers Deep Search, Deep Research on companies and people, semantic search, lookalike search, company and people tables, column operations, enrichment, and contact finding. --- # Extruct API @@ -45,6 +45,7 @@ This section covers the default operating intent of the skill: identify the Extr - if the user provides an Extruct task URL or a raw task UUID, treat it as an existing Deep Search task first - known company lookup: fetch the canonical company profile for one domain or UUID - company discovery: semantic search, lookalike search, or Deep Search + - research report: Deep Research for a deep, cited report on one target — a company, a person, or a team - existing table operation: inspect, add/update rows or columns, run, poll, read - company-table workflow: enrich or score companies in a reusable table - people workflow: find people at companies or enrich existing people rows @@ -84,6 +85,7 @@ If none of those conditions is true, stop before running Pro-only commands and t Pro-only actions in this skill: - Deep Search mutations: `deep-search create`, `deep-search resume`, `deep-search pause` +- Deep Research mutations: `deep-research create` - Table mutations: `tables create`, `tables update`, `tables delete`, `tables clone`, `tables run`, `rows create`, `rows update`, `rows delete`, `columns add`, `columns update`, `columns delete` ## Resolve Extruct Identifiers @@ -316,6 +318,84 @@ If Deep Search payload fields, task states, or resume behavior are unclear, veri Read `references/finding-companies.md` when the task is a fuller company-discovery workflow instead of a single search command. +### Deep Research + +Use Deep Research when the user wants a deep, cited report on one research target — a company, a person, or a team: account planning, buyer research, researching a lead before outreach, initiative summaries, diligence. For discovering many companies, use Deep Search; for repeatable enrichment across a list, use tables. + +`deep-research create` is Pro-gated. Run the plan-access preflight before it. + +Typical asks: + +- "research Shell for me: buying centers, initiatives, sales angles" +- "build an account plan brief for Stripe" +- "research this lead and the team they work with before my call" +- "do diligence on this company and give me a sourced report" + +Write the brief as a detailed paragraph, not a one-liner. Pack in everything you know +from the conversation: what the user sells or researches, who the target buyer is, what +decision the report supports, constraints, and time windows. The more context and +specifics the brief carries, the better the report. If the user's ask is thin, ask one +or two clarifying questions before creating the task. + +Create a task (markdown report): + +```bash + deep-research create --payload '{"brief":"We sell a cloud cost-optimization platform to large enterprises; typical buyers are VPs of Infrastructure and FinOps leads. I am preparing outreach to Shell. Research how Shell'"'"'s IT and digital organization is structured, who owns cloud infrastructure and FinOps decisions, which cloud, data, or efficiency initiatives they announced in the last 18 months, and which vendors or system integrators they already work with. I want practical conversation angles tied to live initiatives, plus any signals of cost-cutting programs or budget pressure.","depth":"medium"}' +``` + +People are first-class targets — include the user's own context and the profile to research: + +```bash + deep-research create --payload '{"brief":"Here is my company: example.com. We sell AI-powered sales-enablement software to mid-market B2B teams. Research this person and the team they work with: https://www.linkedin.com/in/example-profile. I want their role and scope, what their team owns, recent initiatives or public statements, tools they already use, and the best angle to open a conversation.","depth":"medium"}' +``` + +Create a task with structured output (`--payload-file` preferred for schemas): + +```bash + deep-research create --payload-file research.json +``` + +`research.json`: + +```json +{ + "brief": "We provide fraud-prevention APIs for fintech platforms and are building an account plan for Stripe. Summarize Stripe's enterprise product initiatives from the last 12 months (payments, billing, risk, AI), identify concrete product or partnership angles where a fraud-prevention vendor could plug in, and flag risks that could stall a deal, such as in-house solutions, recent vendor consolidation, or compliance constraints.", + "depth": "high", + "output_schema": { + "type": "object", + "properties": { + "summary": {"type": "string"}, + "recommended_angles": {"type": "array", "items": {"type": "string"}}, + "risks": {"type": "array", "items": {"type": "string"}} + }, + "required": ["summary", "recommended_angles", "risks"] + } +} +``` + +Inspect, list, and wait for completion: + +```bash + deep-research list --limit 20 + deep-research get + deep-research poll +``` + +Depth and billing: + +- `depth` sets the research-agent budget: `medium` = 25, `high` = 50, `xhigh` = 75. +- Creating a task requires the full budget in available credits; the user is billed only for agents that actually run (1 credit each). Failed tasks refund all charges. +- If create returns `insufficient_credits` with `required_credits`/`available_credits`, a lower depth may still fit — offer it. + +Reading the result: + +- Tasks take minutes. `deep-research poll` blocks until `status` is `done` or `failed`; progress counters are `iterations`, `agents`, `sources`. +- `report.kind` is `markdown` (citations like `[1]` resolve against `report.sources`) or `schema` (`fields` conforming to the output schema, `basis` mapping each field to supporting source ids, plus `sources`). +- **Always surface `report.degradation_reasons` to the user** — plain-language notes when coverage was reduced (early finalization, failed research agents). Empty means a clean run. +- On `failed`, read `failure_reason` and relay it: a rejected brief includes suggestions for fixing it. Failed tasks are refunded. + +Read `references/deep-research.md` for brief-writing, depth choice, and output-schema design guidance. + ## Operate Existing Tables Use these commands when the user already has a table and wants to inspect it, change rows or columns, run new work, or read results. @@ -610,6 +690,8 @@ Use broader role families for coverage, such as `sales leadership`, and exact ti Use this path when the user already has people rows or already has a generated child `people` table and now wants enrichment, contact data, or derived fields. +For a deep one-off report on a single person or team (rather than repeatable table enrichment), use Deep Research instead — see the Deep Research section above. + Typical asks: - "find work emails for these people" @@ -780,6 +862,7 @@ Check that: - `references/column-guide.md`: column design rules plus a comprehensive library of good column configs - `references/finding-companies.md`: choose and operate semantic search, lookalike, and Deep Search +- `references/deep-research.md`: write briefs, choose depth, and design output schemas for Deep Research - `references/researching-companies.md`: build or extend company research tables safely - `references/finding-people-at-companies.md`: branch from company tables into people workflows - `references/researching-people.md`: enrich standalone or generated people tables diff --git a/skills/extruct-api/agents/openai.yaml b/skills/extruct-api/agents/openai.yaml index 6baf7ec..d189e73 100644 --- a/skills/extruct-api/agents/openai.yaml +++ b/skills/extruct-api/agents/openai.yaml @@ -1,6 +1,6 @@ interface: display_name: "Extruct API" - short_description: "Company discovery, Deep Search, AI tables, enrichment, and people workflows" + short_description: "Company discovery, Deep Search, Deep Research, AI tables, enrichment, and people workflows" icon_small: "./assets/logo.svg" icon_large: "./assets/logo.svg" brand_color: "#4D56F3" diff --git a/skills/extruct-api/references/deep-research.md b/skills/extruct-api/references/deep-research.md new file mode 100644 index 0000000..765e723 --- /dev/null +++ b/skills/extruct-api/references/deep-research.md @@ -0,0 +1,86 @@ +# Deep Research Playbook + +Deep Research turns a free-text brief about one research target — a company, a person, or a team — into a cited report. +A reasoning agent plans the research, fans out research agents that gather evidence, +and synthesizes the result. This playbook covers writing briefs, choosing depth, +designing output schemas, and reading reports honestly. + +## When this path, when another + +- One target, deep sourced report (account plan, buyer research, diligence): Deep Research. +- A person or team — research a lead before outreach or a meeting: Deep Research. +- Many companies matching criteria: Deep Search. +- The same questions across a list of companies, repeatably: a company table. +- One company's profile facts, instantly: company lookup. + +## Write the brief like a request to an analyst + +Good briefs are detailed paragraphs that name the target, the requester's own context, +and the decision the report should support. One-liners produce unfocused reports. + +- Good: "We sell a cloud cost-optimization platform to large enterprises; typical + buyers are VPs of Infrastructure and FinOps leads. I am preparing outreach to Shell. + Research how Shell's IT and digital organization is structured, who owns cloud + infrastructure and FinOps decisions, which cloud, data, or efficiency initiatives + they announced in the last 18 months, and which vendors or system integrators they + already work with. I want practical conversation angles tied to live initiatives, + plus any signals of cost-cutting programs or budget pressure." +- Good: "We are a seed-stage investor evaluating Acme Robotics for a follow-on round. + Build a diligence brief: funding history and investors, key customers and revenue + signals, the competitive landscape for warehouse automation, recent leadership + changes, and open risks we should pressure-test in the partner meeting." +- Good: "Here is my company: example.com. We sell AI-powered sales-enablement software to mid-market B2B teams. Research this person and the team they work with: https://www.linkedin.com/in/example-profile. I want their role and scope, what their team owns, recent initiatives or public statements, tools they already use, and the best angle to open a conversation." +- Bad: "Help me break into Shell." (no context about what you sell or who you target) +- Bad: "Tell me about Stripe." (no decision to support; the report will be unfocused) + +Include everything the research agents cannot guess: what you sell, who your buyer is, +the angle you care about, time windows when freshness matters, what you already know. +For people targets, include the LinkedIn profile URL (or full name plus company) and +your own company and offer — the report is only as targeted as the brief. +The brief can be long (up to 20,000 characters) — pasting context is encouraged, and +more specific briefs reliably produce better reports. + +Vague-but-valid briefs run; truly targetless or unusable briefs are rejected +asynchronously with suggestions in `failure_reason`, and nothing is charged. + +## Choose depth deliberately + +- `medium` (25 agents): default. Right for most account plans and single-question research. +- `high` (50): multi-angle research on a large target, or schema mode with many fields. +- `xhigh` (75): exhaustive coverage; use when the user explicitly wants maximum depth. + +Creating a task requires the full budget in available credits, but billing is per agent +that actually runs — a focused brief at `high` often finishes well under budget. If +create fails with `insufficient_credits`, the error includes `required_credits` and +`available_credits`; offer a lower depth. + +## Design output schemas narrowly + +Schema mode (`output_schema`) is for machine-readable reports. Rules of thumb: + +- A handful of fields, each naming one decision-relevant fact or bounded list. +- Use arrays of strings for angles, risks, initiatives; strings for summaries; numbers + only for genuinely numeric facts. +- The schema must be a JSON Schema object (`"type": "object"`); it is validated in + full at creation time, so a malformed nested schema fails fast. +- A `done` schema report is guaranteed to conform. Per-field `basis` (source ids) is + the audit trail — treat fields with an empty basis as unsupported. + +## Run and read + +```bash + deep-research create --payload-file research.json + deep-research poll +``` + +Reading the report honestly: + +- Markdown reports cite sources as `[1]`-style ids resolving against `report.sources`. + Do not strip the citations when relaying to the user. +- **Always check `report.degradation_reasons`** and show them with the report. They say, + in plain language, when research stopped early (budget or step limit) or when some + research agents failed. An empty list means a clean run. +- On `failed`, relay `failure_reason`. Rejected briefs include concrete suggestions — + offer to retry with a fixed brief. Failed tasks refund all their charges. +- Progress while running: `iterations` (analysis steps), `agents` (billed research + agents), `sources` (unique sources collected). diff --git a/skills/extruct-api/scripts/extruct-api b/skills/extruct-api/scripts/extruct-api index 41876e4..3d475bf 100755 --- a/skills/extruct-api/scripts/extruct-api +++ b/skills/extruct-api/scripts/extruct-api @@ -35,6 +35,8 @@ DEFAULT_POLL_MAX_ATTEMPTS = 60 DEFAULT_DEEP_SEARCH_RESULTS_LIMIT = 50 DEFAULT_DEEP_SEARCH_POLL_INTERVAL_SECONDS = 10.0 DEFAULT_DEEP_SEARCH_POLL_MAX_ATTEMPTS = 120 +DEFAULT_DEEP_RESEARCH_POLL_INTERVAL_SECONDS = 15.0 +DEFAULT_DEEP_RESEARCH_POLL_MAX_ATTEMPTS = 240 ALLOWED_TABLE_SORT_VALUES = ("-created_at", "created_at", "name", "-name", "num_rows", "-num_rows") EXIT_SUCCESS = 0 @@ -484,6 +486,12 @@ def _deep_search_is_complete(payload: Any) -> bool: return payload.get("status") in ("done", "failed") +def _deep_research_is_complete(payload: Any) -> bool: + if not isinstance(payload, dict): + return False + return payload.get("status") in ("done", "failed") + + def _validate_payload_object(payload: Any, *, path: str) -> tuple[Any | None, int]: if isinstance(payload, dict): return None, EXIT_SUCCESS @@ -493,6 +501,34 @@ def _validate_payload_object(payload: Any, *, path: str) -> tuple[Any | None, in ) +def _validate_deep_research_create_payload(payload: Any, *, path: str) -> tuple[Any | None, int]: + payload_error, payload_code = _validate_payload_object(payload, path=path) + if payload_error is not None: + return payload_error, payload_code + + brief = payload.get("brief") + if not isinstance(brief, str) or not brief.strip(): + return _cli_error( + message="Invalid payload: 'brief' is required and must be a non-empty string.", + path=path, + ) + + if "depth" in payload and payload["depth"] not in ("medium", "high", "xhigh"): + return _cli_error( + message="Invalid payload: 'depth' must be one of 'medium', 'high', 'xhigh'.", + path=path, + ) + + if "output_schema" in payload and payload["output_schema"] is not None: + if not isinstance(payload["output_schema"], dict): + return _cli_error( + message="Invalid payload: 'output_schema' must be a JSON Schema object.", + path=path, + ) + + return None, EXIT_SUCCESS + + def _validate_deep_search_create_payload(payload: Any, *, path: str) -> tuple[Any | None, int]: payload_error, payload_code = _validate_payload_object(payload, path=path) if payload_error is not None: @@ -905,6 +941,44 @@ def _build_parser() -> argparse.ArgumentParser: deep_search_poll.add_argument("--interval", type=float, default=DEFAULT_DEEP_SEARCH_POLL_INTERVAL_SECONDS) deep_search_poll.add_argument("--max-attempts", type=int, default=DEFAULT_DEEP_SEARCH_POLL_MAX_ATTEMPTS) + deep_research = resources.add_parser( + "deep-research", + help="Deep Company Research task commands. Tasks are asynchronous and produce one cited report.", + parents=[global_options], + ) + deep_research_actions = deep_research.add_subparsers(dest="action", required=True) + + deep_research_list = deep_research_actions.add_parser( + "list", help="List Deep Research tasks, newest first.", parents=[global_options] + ) + deep_research_list.add_argument("--offset", type=int, default=DEFAULT_LIST_OFFSET) + deep_research_list.add_argument("--limit", type=int, default=DEFAULT_LIST_LIMIT) + + deep_research_create = deep_research_actions.add_parser( + "create", + help="Create a Deep Research task. Requires payload with non-empty brief.", + parents=[global_options], + ) + _add_payload_args(deep_research_create) + + deep_research_get = deep_research_actions.add_parser( + "get", help="Get a Deep Research task.", parents=[global_options] + ) + deep_research_get.add_argument("task_id") + + deep_research_poll = deep_research_actions.add_parser( + "poll", + help="Poll a Deep Research task until status=done or status=failed.", + parents=[global_options], + ) + deep_research_poll.add_argument("task_id") + deep_research_poll.add_argument( + "--interval", type=float, default=DEFAULT_DEEP_RESEARCH_POLL_INTERVAL_SECONDS + ) + deep_research_poll.add_argument( + "--max-attempts", type=int, default=DEFAULT_DEEP_RESEARCH_POLL_MAX_ATTEMPTS + ) + return parser @@ -1006,6 +1080,18 @@ def _deep_search_method_path(action: str, task_id: str | None = None) -> tuple[s raise ValueError(f"Unsupported deep-search action: {action}") +def _deep_research_method_path(action: str, task_id: str | None = None) -> tuple[str, str]: + if action == "list": + return "GET", "/v1/deep_research_tasks" + if action == "create": + return "POST", "/v1/deep_research_tasks" + if task_id is None: + raise ValueError("task_id is required for this deep-research action.") + if action in ("get", "poll"): + return "GET", f"/v1/deep_research_tasks/{task_id}" + raise ValueError(f"Unsupported deep-research action: {action}") + + def _handle_auth(args: argparse.Namespace) -> int: method, path = _auth_method_path(args.action) return _execute_request(args, method=method, path=path) @@ -1444,6 +1530,95 @@ def _handle_deep_search(args: argparse.Namespace) -> int: return code +def _handle_deep_research(args: argparse.Namespace) -> int: + if args.action == "list": + method, path = _deep_research_method_path("list") + pagination_error, pagination_error_code = _validate_pagination(args.offset, args.limit, path=path) + if pagination_error is not None: + _json_print(pagination_error, args.pretty) + return pagination_error_code + return _execute_request( + args, + method=method, + path=path, + query={"offset": args.offset, "limit": args.limit}, + ) + + if args.action == "create": + method, path = _deep_research_method_path("create") + payload, payload_error, payload_code = _load_payload(args.payload, args.payload_file, path=path) + if payload_error is not None: + _json_print(payload_error, args.pretty) + return payload_code + validation_error, validation_code = _validate_deep_research_create_payload(payload, path=path) + if validation_error is not None: + _json_print(validation_error, args.pretty) + return validation_code + return _execute_request(args, method=method, path=path, payload=payload) + + task_error, task_code = _validate_non_empty( + args.task_id, + flag="task_id", + path="/v1/deep_research_tasks/{task_id}", + ) + if task_error is not None: + _json_print(task_error, args.pretty) + return task_code + + method, path = _deep_research_method_path(args.action, task_id=args.task_id) + + if args.action == "get": + return _execute_request(args, method=method, path=path) + + if args.action == "poll": + interval_error, interval_code = _validate_positive_number(args.interval, flag="--interval", path=path) + if interval_error is not None: + _json_print(interval_error, args.pretty) + return interval_code + attempts_error, attempts_code = _validate_positive_number( + args.max_attempts, + flag="--max-attempts", + path=path, + ) + if attempts_error is not None: + _json_print(attempts_error, args.pretty) + return attempts_code + + token, token_error, token_exit = _resolve_token(method=method, path=path) + if token is None: + _json_print(token_error, args.pretty) + return token_exit + + for attempt in range(args.max_attempts): + response_payload, exit_code = _make_request( + base_url=args.base_url, + token=token, + method=method, + path=path, + timeout=args.timeout, + ) + if exit_code != EXIT_SUCCESS: + _json_print(response_payload, args.pretty) + return exit_code + if _deep_research_is_complete(response_payload): + _json_print(response_payload, args.pretty) + return EXIT_SUCCESS + if attempt + 1 < args.max_attempts: + time.sleep(args.interval) + + timeout_error, timeout_code = _cli_error( + message="Deep Research polling timed out before reaching a terminal state.", + path=path, + details={"max_attempts": args.max_attempts, "interval": args.interval}, + ) + _json_print(timeout_error, args.pretty) + return timeout_code + + error, code = _cli_error(message="Unsupported deep-research action.", path="/v1/deep_research_tasks") + _json_print(error, args.pretty) + return code + + def main() -> int: parser = _build_parser() args = parser.parse_args(_normalize_argv(sys.argv[1:])) @@ -1463,6 +1638,8 @@ def main() -> int: return _handle_columns(args) if args.resource == "deep-search": return _handle_deep_search(args) + if args.resource == "deep-research": + return _handle_deep_research(args) error, code = _cli_error(message="Unsupported resource.", path="/") _json_print(error, args.pretty)