Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ jobs:
python-version: '3.14'
- run: uv sync --all-packages
# SDK features land on development before the platform deploys to
# prod, so PRs targeting development check against the dev spec.
# prod, so every PR but a release into main checks against the dev
# spec — including one stacked on another feature branch, which is
# just as far ahead of prod as the branch it targets.
# DEV_SPEC_URL is a repo secret; fork PRs don't receive it and
# fall back to the prod spec, keeping dev infra internal-only.
- env:
BASE_REF: ${{ github.base_ref }}
DEV_SPEC_URL: ${{ secrets.DEV_SPEC_URL }}
run: |
if [ "$BASE_REF" = "development" ] && [ -n "$DEV_SPEC_URL" ]; then
if [ "$BASE_REF" != "main" ] && [ -n "$DEV_SPEC_URL" ]; then
uv run python scripts/check_contract.py --spec-url "$DEV_SPEC_URL"
uv run python scripts/gen_requests.py --check --spec-url "$DEV_SPEC_URL"
else
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- SDK: `Job` / `AsyncJob` returned by `validate_icp`, `discogen.process` and `discogen.process_personas` carry `column_name` from the submit response. The engine picks the columns — an LLM validation returns `Fit` / `Confidence` / `Reasoning`, the native model `ICP Fit` (`Yes` / `No` at a 0.50 threshold on the score) / `ICP Score` (the calibrated probability, 0.00-1.00 as a string) / `Reasoning (always null)` — so read it rather than hardcoding either set. It is `None` on a job reattached with `discogen.job(task_id)`.
- CLI: `validate-icp --integration-id native-icp` and `discogen run --integration-id native-icp` select the native ICP-fit model; `discolike validate-icp --help` documents both column sets.
- **Breaking:** the ICP-fit verdict is title case on every surface. An LLM `validate_icp` run's `Fit` column now returns `Yes` / `No` instead of `yes` / `no`, matching the native engine's `ICP Fit` column, DiscoGen and the app. Code that matches the verdict on the exact string `"yes"` has to be updated; compare case-insensitively to survive either.
- SDK/CLI: `discogen.process` and `discogen.process_personas` take `typed_columns`, and the CLI gains `--typed-columns`. It opts the query into the detector answering yes/no, fixed-set and scale columns with a TypeSafe judgment model instead of generated prose; which columns are typed and how is decided server-side from the query text, not passed in by the caller. Off by default. See `include_confidence` below for the sibling flag that adds a confidence column to each typed column.
- SDK/CLI: `discogen.process` and `discogen.process_personas` take `include_confidence`, and the CLI gains `--include-confidence`. It applies to typed columns, which a DiscoGen run answers with a TypeSafe judgment model rather than generated prose; with it on, each typed column gets a sibling confidence column. Off by default, and it changes display only - the answers and their probabilities come back either way.
- SDK: `contacts.generate` can run without an LLM key. Pass `integration_id=NATIVE_ENGINE` (exported from `discolike`) for DiscoLike Groove, DiscoLike's native extractor, or omit `integration_id` to get your default LLM integration and native when you have none. The native engine returns every person the search surfaces with a title and does not validate titles against `icp_text`; a search provider is still required. `JobStatus` gains `title_validation` (`"llm"` or `"none"`, `None` on non-ContaGen jobs) so you can tell which engine ran.
- SDK: discover and count take `sub_industry` / `negate_sub_industry` (217 second-level labels scoped to their parent industry category; a bare label like `ROOFING` adds `CONSTRUCTION` to `category` server-side) and a multi-shape geo filter: `geo` (`lat,lon` or `lat,lon,radius`), `bbox` (`min_lat,min_lon,max_lat,max_lon`, longitudes may wrap the antimeridian) and `lat` / `lon` / `radius` (`50km`, `30mi`, or a bare number meaning kilometres; defaults to 50km). `geo` and `bbox` are lists, every circle and box is OR'd with the others and with the `lat`/`lon` centre, and a query carries at most 10 shapes. Passing a single `bbox` or `geo` string still works and is sent as a one-element list. The SDK applies the platform's own shape rules before the request leaves the client: a box is four finite numbers with `-90 <= min_lat < max_lat <= 90`, longitudes within ±180 and `min_lon` different from `max_lon`; a circle is a valid coordinate pair with an optional radius above 0 and at most 1000km.
- SDK: `CompanyProfile` gains `sub_industry` (label:confidence, `None` when the domain was never scored against its category's sub-labels), `latitude`, `longitude` and `geo_precision`. The coordinates come back under their full names, and a response still using the pre-rename `lat` / `lon` keys parses into them, so the SDK works against an API on either side of that release. The `lat` / `lon` discover and count filters keep their names.
Expand Down
14 changes: 14 additions & 0 deletions packages/discolike-cli/src/discolike_cli/discogen.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
WEB_SEARCH_HELP = "Toggle web search during research."
CONTEXT_MODE_HELP = "Context mode; see docs.discolike.com."
INCLUDE_X_SEARCH_HELP = "Toggle including X search in the research."
TYPED_COLUMNS_HELP = "Let the detector answer yes/no, fixed-set and scale columns with a TypeSafe judgment model."
INCLUDE_CONFIDENCE_HELP = "Add a confidence column beside each typed column."
SEARCH_PROVIDER_ID_HELP = "Search provider ID to use for web search."
SEARCH_CONTEXT_SIZE_HELP = "Search context size; see docs.discolike.com."
TASK_ID_HELP = "Task ID returned when the job was started."
Expand Down Expand Up @@ -62,6 +64,10 @@ def run_command(
include_x_search: bool | None = typer.Option(
None, "--include-x-search/--no-include-x-search", help=INCLUDE_X_SEARCH_HELP
),
typed_columns: bool | None = typer.Option(None, "--typed-columns/--no-typed-columns", help=TYPED_COLUMNS_HELP),
include_confidence: bool | None = typer.Option(
None, "--include-confidence/--no-include-confidence", help=INCLUDE_CONFIDENCE_HELP
),
Comment thread
yudelevi marked this conversation as resolved.
search_provider_id: str | None = typer.Option(None, "--search-provider-id", help=SEARCH_PROVIDER_ID_HELP),
search_context_size: str | None = typer.Option(None, "--search-context-size", help=SEARCH_CONTEXT_SIZE_HELP),
wait: bool = typer.Option(False, "--wait", help=WAIT_HELP),
Expand All @@ -84,6 +90,8 @@ def run_command(
web_search=web_search,
context_mode=context_mode,
include_x_search=include_x_search,
typed_columns=typed_columns,
include_confidence=include_confidence,
search_provider_id=search_provider_id,
search_context_size=search_context_size,
),
Expand All @@ -103,6 +111,10 @@ def run_personas_command(
include_x_search: bool | None = typer.Option(
None, "--include-x-search/--no-include-x-search", help=INCLUDE_X_SEARCH_HELP
),
typed_columns: bool | None = typer.Option(None, "--typed-columns/--no-typed-columns", help=TYPED_COLUMNS_HELP),
include_confidence: bool | None = typer.Option(
None, "--include-confidence/--no-include-confidence", help=INCLUDE_CONFIDENCE_HELP
),
search_provider_id: str | None = typer.Option(None, "--search-provider-id", help=SEARCH_PROVIDER_ID_HELP),
search_context_size: str | None = typer.Option(None, "--search-context-size", help=SEARCH_CONTEXT_SIZE_HELP),
wait: bool = typer.Option(False, "--wait", help=WAIT_HELP),
Expand All @@ -122,6 +134,8 @@ def run_personas_command(
web_search=web_search,
context_mode=context_mode,
include_x_search=include_x_search,
typed_columns=typed_columns,
include_confidence=include_confidence,
search_provider_id=search_provider_id,
search_context_size=search_context_size,
),
Expand Down
172 changes: 172 additions & 0 deletions packages/discolike-cli/tests/test_discogen_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,91 @@ def handler(request: httpx2.Request) -> httpx2.Response:
}


def test_discogen_run_sends_typed_columns_when_passed(install_build_client: Callable[[Handler], None]) -> None:
captured: dict[str, object] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-1c"})

install_build_client(handler)
result = runner.invoke(
app,
["discogen", "run", "--query", "q", "--domain", "acme.com", "--typed-columns"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {
"query": "q",
"domains": ["acme.com"],
"typed_columns": True,
}


def test_discogen_run_sends_include_confidence_true_when_flag_passed(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, object] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-1d"})

install_build_client(handler)
result = runner.invoke(
app,
["discogen", "run", "--query", "q", "--domain", "acme.com", "--include-confidence"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {
"query": "q",
"domains": ["acme.com"],
"include_confidence": True,
}


def test_discogen_run_sends_include_confidence_false_when_flag_negated(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, object] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-1e"})

install_build_client(handler)
result = runner.invoke(
app,
["discogen", "run", "--query", "q", "--domain", "acme.com", "--no-include-confidence"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {
"query": "q",
"domains": ["acme.com"],
"include_confidence": False,
}


def test_discogen_run_omits_include_confidence_when_flag_not_passed(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, object] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-1f"})

install_build_client(handler)
result = runner.invoke(
app,
["discogen", "run", "--query", "q", "--domain", "acme.com"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {
"query": "q",
"domains": ["acme.com"],
}


def test_discogen_run_personas_posts_persona_ids(install_build_client: Callable[[Handler], None]) -> None:
captured: dict[str, object] = {}

Expand Down Expand Up @@ -110,6 +195,93 @@ def handler(request: httpx2.Request) -> httpx2.Response:
}


def test_discogen_run_personas_sends_typed_columns_when_passed(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, object] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-2c"})

install_build_client(handler)
result = runner.invoke(
app,
["discogen", "run-personas", "--query", "q", "--persona-id", "1", "--typed-columns"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {
"query": "q",
"persona_ids": [1],
"typed_columns": True,
}


def test_discogen_run_personas_sends_include_confidence_true_when_flag_passed(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, object] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-2c"})

install_build_client(handler)
result = runner.invoke(
app,
["discogen", "run-personas", "--query", "q", "--persona-id", "1", "--include-confidence"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {
"query": "q",
"persona_ids": [1],
"include_confidence": True,
}


def test_discogen_run_personas_sends_include_confidence_false_when_flag_negated(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, object] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-2d"})

install_build_client(handler)
result = runner.invoke(
app,
["discogen", "run-personas", "--query", "q", "--persona-id", "1", "--no-include-confidence"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {
"query": "q",
"persona_ids": [1],
"include_confidence": False,
}


def test_discogen_run_personas_omits_include_confidence_when_flag_not_passed(
install_build_client: Callable[[Handler], None],
) -> None:
captured: dict[str, object] = {}

def handler(request: httpx2.Request) -> httpx2.Response:
captured["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-2e"})

install_build_client(handler)
result = runner.invoke(
app,
["discogen", "run-personas", "--query", "q", "--persona-id", "1"],
)
assert result.exit_code == 0, result.output
assert captured["body"] == {
"query": "q",
"persona_ids": [1],
}


def test_discogen_run_with_wait_polls_to_completion(install_build_client: Callable[[Handler], None]) -> None:
statuses = iter(
[
Expand Down
22 changes: 19 additions & 3 deletions packages/discolike/src/discolike/_generated/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1421,12 +1421,20 @@ class DiscoGenProcessRequest(DiscolikeRequest):
integration_id: Annotated[
str | None,
Field(
description="LLM provider integration UUID (omit for org default)",
description="LLM provider integration UUID (omit for org default), or 'native-icp' to score an ICP validation prompt with the native model",
title="Integration Id",
),
] = None
web_search: Annotated[bool | None, Field(title="Web Search")] = False
include_x_search: Annotated[bool | None, Field(title="Include X Search")] = False
typed_columns: Annotated[
bool | None,
Field(
description="Let the detector answer yes/no, fixed-set and scale columns with a TypeSafe judgment model",
title="Typed Columns",
),
] = False
include_confidence: Annotated[bool | None, Field(title="Include Confidence")] = False
search_provider_id: Annotated[
str | None,
Field(
Expand Down Expand Up @@ -1456,12 +1464,20 @@ class DiscoGenPersonaProcessRequest(DiscolikeRequest):
integration_id: Annotated[
str | None,
Field(
description="LLM provider integration UUID (omit for org default)",
description="LLM provider integration UUID (omit for org default), or 'native-icp' to score an ICP validation prompt with the native model",
title="Integration Id",
),
] = None
web_search: Annotated[bool | None, Field(title="Web Search")] = False
include_x_search: Annotated[bool | None, Field(title="Include X Search")] = False
typed_columns: Annotated[
bool | None,
Field(
description="Let the detector answer yes/no, fixed-set and scale columns with a TypeSafe judgment model",
title="Typed Columns",
),
] = False
include_confidence: Annotated[bool | None, Field(title="Include Confidence")] = False
search_provider_id: Annotated[
str | None,
Field(
Expand Down Expand Up @@ -1507,7 +1523,7 @@ class ValidateIcpRequest(DiscolikeRequest):
integration_id: Annotated[
str | None,
Field(
description="LLM provider integration UUID (omit for org default)",
description="LLM provider integration UUID (omit for org default), or 'native-icp' to score with DiscoLike's own ICP-fit model at no LLM cost",
title="Integration Id",
),
] = None
Expand Down
17 changes: 17 additions & 0 deletions packages/discolike/tests/test_discogen.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ def handler(request: httpx2.Request) -> httpx2.Response:
web_search=True,
context_mode="website",
include_x_search=False,
typed_columns=True,
include_confidence=True,
search_provider_id="serper",
search_context_size="medium",
)
Expand All @@ -84,11 +86,26 @@ def handler(request: httpx2.Request) -> httpx2.Response:
"web_search": True,
"context_mode": "website",
"include_x_search": False,
"typed_columns": True,
"include_confidence": True,
"search_provider_id": "serper",
"search_context_size": "medium",
}


def test_process_sends_typed_columns_when_set(make_client: ClientFactory) -> None:
seen = {}

def handler(request: httpx2.Request) -> httpx2.Response:
seen["body"] = json.loads(request.content)
return httpx2.Response(200, json={"task_id": "dg-typed"})

with make_client(handler) as client:
client.discogen.process(DiscoGenProcessRequest(query="q", domains=["a.com"], typed_columns=True))

assert seen["body"] == {"query": "q", "domains": ["a.com"], "typed_columns": True}


def test_process_personas_posts_json_and_returns_job(make_client: ClientFactory) -> None:
seen = {}

Expand Down
Loading