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
5 changes: 5 additions & 0 deletions .changeset/kind-files-throw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apollo": minor
---

update dependencies
24 changes: 24 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Dependabot configuration
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2

updates:
# Python / Poetry dependencies
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
# Group all minor and patch updates into a single PR to reduce noise
groups:
python-minor-patch:
update-types:
- "minor"
- "patch"

# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
948 changes: 331 additions & 617 deletions poetry.lock

Large diffs are not rendered by default.

19 changes: 11 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,28 @@ requires-poetry = ">=2.3.2"

[tool.poetry.dependencies]
python = "3.11.*"
openai = "^1.23.3"
openai = "^2.26"
python-dotenv = "^1.2.2"
anthropic = "^0.85.0"
anthropic = "^0.107.1"

langchain-pinecone = "0.2.2"
langchain-community = "^0.3.27"
langchain-openai = "^0.3.1"
langchain-pinecone = "^0.2.13"
langchain-core = "^1.4"
langchain-community = "^0.4.2"
langchain-openai = "^1.2"
langchain-text-splitters = "^1.1"
nltk = "^3.9.3"
pytest = "^8.4.1"
pytest = "^9.0.3"
sentry-sdk = "^2.35.0"
psycopg2-binary = "^2.9.10"
langfuse = "^4.0.1"
opentelemetry-instrumentation-anthropic = "^0.53.3"
opentelemetry-instrumentation-anthropic = "^0.61.0"
opentelemetry-instrumentation-threading = "0.61b0"

[tool.poetry.group.dev]
optional = false

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.4"
pytest = "^9.0.3"
ruff = "^0.15.10"

[build-system]
Expand All @@ -46,6 +48,7 @@ testpaths = [
"services/global_chat/tests",
"services/workflow_chat/tests",
"services/job_chat/tests",
"services/search_docsite/tests",
"services/tools",
]

Expand Down
4 changes: 2 additions & 2 deletions services/embeddings/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import warnings
from dotenv import load_dotenv
from langchain_community.vectorstores import Zilliz
from langchain_pinecone import Pinecone
from langchain_pinecone import PineconeVectorStore
from langchain_openai import OpenAIEmbeddings

@dataclass
Expand Down Expand Up @@ -44,7 +44,7 @@ class VectorStore:
}
VECTORSTORE_CLASSES = {
'zilliz': Zilliz,
'pinecone': Pinecone
'pinecone': PineconeVectorStore
}

def __init__(self, collection_name='LangChainCollection', vectorstore_type='zilliz',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
id: job-chat.tmp.repro-dhis2-console-log
service: job_chat
judges: [general, openfn_code_quality]
---

# notes

Faithful reproduction of production Sentry issue APOLLO-6H (event 8374b920,
`@openfn/language-dhis2`, via global_chat's router → job_chat with
`suggest_code=true`, `stream=true`). The model returned no usable text
(`stop_reason=end_turn`, `empty_reason=no_text_blocks`) and the service raised
`ApolloError(502, EMPTY_OUTPUT)`.

Unlike the `repro-dots-response` teach-me case, this is a pure
**debugging / conversational** turn: the user is asking *why* their console.log
shows nothing. The ideal answer is mostly prose that diagnoses the data shape —
little or no code change is required. This is the conversational-empty flavour
of the same bug, run with `stream=true` to match production exactly.

Expected behaviour: a substantive diagnostic answer. The response must NOT be
empty / "..." and must NOT silently rewrite the whole job — at most a targeted
tweak to the logging/data-access line.

# quality_criteria

- The response is non-empty and substantive — explicitly NOT "..." , empty, or a single short fragment.
- The response diagnoses why the `console.log` shows nothing — e.g. `state.data.instances` may be the wrong access path or the result set is empty, and suggests inspecting `state.data` to confirm the actual response shape.
- The response engages with the established conversation context (DHIS2 `tracker/trackedEntities` returning results under `instances`) rather than ignoring it.
- If it proposes a code change, it is a targeted fix to the logging / data-access line, NOT a wholesale rewrite that discards the user's existing `get()` operation.

# settings

## context.expression

```js
get('tracker/trackedEntities', {
orgUnits: 'DiszpKrYNg8',
program: 'IpHINAT79UW',
paging: false
});

fn(state => {
console.log(state.data.instances);
return state;
});
```

## context.adaptor

@openfn/[email protected]

## context.page_name

Get tracked entities since last run

## suggest_code

true

## stream

true

## meta.session_id

sess-tmp-repro-dhis2-console-log-0001

## meta.user.id

user-hanna-repro-0002

## meta.user.role

core_contributor

# history

## turn

### role

user

### content

i'd like to fetch TEIs with these attributes: orgUnitId=DiszpKrYNg8&programId=IpHINAT79UW

and console log all of them.

## turn

### role

assistant

### content

This fetches all TEIs for the given org unit and program, then logs them. `paging: false` ensures all records are returned without pagination.

## turn

### role

user

### content

i don't see any changes here. did you try to adjust the code in this job?

## turn

### role

assistant

### content

Sorry about that — here's the updated code now.

## turn

### role

user

### content

that doesn't work, check my errors.

i can see a list on the web UI from this URL - is that helpful?


https://dev.im.dhis2.org/dhis2-openfn-opencrvs/api/43/tracker/trackedEntities?order=createdAt%3Adesc&page=1&pageSize=15&orgUnits=FwKJ7gYEv8U&orgUnitMode=SELECTED&program=IpHINAT79UW&fields=%3Aall%2C!relationships%2CprogramOwners%5BorgUnit%2Cprogram%5D

## turn

### role

assistant

### content

The issue is that newer DHIS2 versions (v40+) use the `tracker/trackedEntities` endpoint instead of `trackedEntityInstances`. The response also uses `instances` instead of `trackedEntityInstances`. Note the URL also uses `orgUnits` (plural).

# turn

## role

user

## content

ok, now why can't i see them in the console.log statement?
14 changes: 14 additions & 0 deletions services/job_chat/tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Test config for job_chat unit tests.

Importing `job_chat.retrieve_docs` pulls in `search_docsite.search_docsite`, whose
module-level `OpenAIEmbeddings()` default arg validates credentials at construction
(openai 2.x / langchain-openai 1.x). A key must therefore exist at import time.

Dummy placeholders only — unit tests mock every network seam, and the repo-root
conftest blocks real client construction. `setdefault` lets a real key win.
"""

import os

os.environ.setdefault("OPENAI_API_KEY", "sk-test-dummy")
os.environ.setdefault("PINECONE_API_KEY", "pc-test-dummy")
78 changes: 78 additions & 0 deletions services/job_chat/tests/unit/test_retrieve_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Unit tests for job_chat.retrieve_docs pure-logic + LLM-call helpers.

Covers the parts that don't need live services:
- generate_queries: JSON parsing, 4-query truncation, and the invalid-JSON guard
- call_llm: happy path (text + usage) and the unexpected-error wrapper

The anthropic client is mocked; `call_llm` is patched where only its result
matters. Importing this module also exercises retrieve_docs' top-level
`from anthropic import APIConnectionError, BadRequestError, ...` — a dependency
contract that breaks loudly if any of those exception classes were renamed.
"""

import json
from unittest.mock import MagicMock, patch

import pytest

from job_chat import retrieve_docs as rd
from util import ApolloError


# --- generate_queries ----------------------------------------------------------

def test_generate_queries_truncates_to_four():
payload = json.dumps({"queries": [{"query": f"q{i}"} for i in range(6)]})
with patch.object(rd, "call_llm", return_value=(payload, {"input_tokens": 1})):
queries, usage = rd.generate_queries("content", client=MagicMock())

assert len(queries) == 4
assert queries[0] == {"query": "q0"}
assert usage == {"input_tokens": 1}


def test_generate_queries_raises_apollo_error_on_invalid_json():
with patch.object(rd, "call_llm", return_value=("not json", {})):
with pytest.raises(ApolloError) as exc:
rd.generate_queries("content", client=MagicMock())

assert exc.value.code == 500
assert exc.value.type == "INVALID_LLM_RESPONSE"


# --- call_llm ------------------------------------------------------------------

def test_call_llm_returns_text_and_usage_on_success():
message = MagicMock()
message.content = [MagicMock(text="hello")]
message.usage.model_dump.return_value = {"input_tokens": 5, "output_tokens": 2}
client = MagicMock()
client.messages.create.return_value = message

text, usage = rd.call_llm(
model="claude-haiku-4-5",
temperature=0,
system_prompt="sys",
user_prompt="usr",
client=client,
)

assert text == "hello"
assert usage == {"input_tokens": 5, "output_tokens": 2}


def test_call_llm_wraps_unexpected_error_as_apollo_error():
client = MagicMock()
client.messages.create.side_effect = ValueError("boom")

with pytest.raises(ApolloError) as exc:
rd.call_llm(
model="claude-haiku-4-5",
temperature=0,
system_prompt="sys",
user_prompt="usr",
client=client,
)

assert exc.value.code == 500
assert exc.value.type == "UNKNOWN_ERROR"
Empty file.
Empty file.
16 changes: 16 additions & 0 deletions services/search_docsite/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Test config for search_docsite.

`search_docsite.search_docsite` constructs `OpenAIEmbeddings()` as a module-level
default argument, which (under openai 2.x / langchain-openai 1.x) validates
credentials at construction time. That happens when the test module is imported,
before any test runs — so a key must exist in the environment or import fails.

These are dummy placeholders only: unit tests inject mocks for every network
seam and the repo-root conftest additionally blocks real client construction, so
no real key is ever used. `setdefault` means a real key (from services/.env) wins.
"""

import os

os.environ.setdefault("OPENAI_API_KEY", "sk-test-dummy")
os.environ.setdefault("PINECONE_API_KEY", "pc-test-dummy")
Empty file.
Loading
Loading