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
2 changes: 1 addition & 1 deletion src/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ class HistoryEntry(BaseModel):
}


SourceSystemValue = Literal["GITHUB", "JIRA", "CONFLUENCE", "UPLOAD"]
SourceSystemValue = Literal["GITHUB", "BITBUCKET", "JIRA", "CONFLUENCE", "UPLOAD"]


class ProjectScopedRequest(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions src/rag/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from ingestion.source_role import SourceRole

ChunkKind = Literal["text", "code", "pdf", "image"]
SourceSystem = Literal["GITHUB", "JIRA", "CONFLUENCE", "UPLOAD"]
SourceSystem = Literal["GITHUB", "BITBUCKET", "JIRA", "CONFLUENCE", "UPLOAD"]


def is_chunk_kind(value: str) -> TypeGuard[ChunkKind]:
return value in ("text", "code", "pdf", "image")


def is_source_system(value: str) -> TypeGuard[SourceSystem]:
return value in ("GITHUB", "JIRA", "CONFLUENCE", "UPLOAD")
return value in ("GITHUB", "BITBUCKET", "JIRA", "CONFLUENCE", "UPLOAD")


@dataclass(frozen=True)
Expand Down
60 changes: 60 additions & 0 deletions tests/api/test_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,66 @@ def stream(self, messages: list[Message]) -> Iterator[LLMStreamEvent]:
]


@pytest.mark.parametrize("value", ["BITBUCKET", "bitbucket"])
def test_chat_accepts_a_bitbucket_source_filter(
client: tuple[TestClient, StubLLMClient, StubVectorStore], value: str
) -> None:
http_client, _, store = client
embedding = [1.0] + [0.0] * 767

class AnsweringLLM(StubLLMClient):
def stream(self, messages: list[Message]) -> Iterator[LLMStreamEvent]:
yield TextDelta("From Bitbucket.")

app.dependency_overrides[get_llm] = lambda: AnsweringLLM(embedding=embedding)
store.add(
[
Chunk(
id="chunk-1",
artifact_id="doc-1",
filename="widgets.md",
text="The widgets service retries three times.",
embedding=embedding,
project_ids=(_PROJECT,),
source_system="BITBUCKET",
)
]
)

response = http_client.post(
"/api/v1/chat",
json={
"prompt": "How does widgets retry?",
"projectId": _PROJECT,
"filters": {"source_systems": [value]},
},
)

# Before BITBUCKET was a known source system this was rejected with a 422.
assert response.status_code == 200
events = parse_sse_events(response.text)
assert [event["content"] for event in events if event["type"] == "token"] == [
"From Bitbucket."
]


def test_chat_still_rejects_an_unknown_source_filter(
client: tuple[TestClient, StubLLMClient, StubVectorStore],
) -> None:
http_client, _, _ = client

response = http_client.post(
"/api/v1/chat",
json={
"prompt": "Anything?",
"projectId": _PROJECT,
"filters": {"source_systems": ["GITLAB"]},
},
)

assert response.status_code == 422


def test_chat_emits_citation_when_chunks_exist(
client: tuple[TestClient, StubLLMClient, StubVectorStore],
) -> None:
Expand Down
36 changes: 35 additions & 1 deletion tests/rag/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
decode_project_ids,
encode_project_ids,
matches_retrieval_filters,
normalize_source_system,
where_filter_for_chroma,
)
from rag.types import Chunk, RetrievalFilters
from rag.types import Chunk, RetrievalFilters, is_source_system


def test_source_system_filter_matches_allowed_system() -> None:
Expand Down Expand Up @@ -54,6 +55,39 @@ def test_confluence_source_system_survives_ingest_and_filtering() -> None:
)


def test_bitbucket_source_system_survives_ingest_and_filtering() -> None:
# Without BITBUCKET in the known systems the ingest silently tagged the chunk
# with no source system, so a Bitbucket filter could never find it.
chunk = to_chunk(
ParsedChunk(
content="Why the widgets service retries",
kind="text",
metadata={"filename": "widgets.md"},
),
artifact_id="bitbucket:acme/widgets:FILE:widgets.md",
embedding=[1.0, 0.0],
artifact_type="FILE",
source_system="BITBUCKET",
)

assert chunk.source_system == "BITBUCKET"
assert matches_retrieval_filters(
chunk,
RetrievalFilters(source_systems=["BITBUCKET"]),
)
assert not matches_retrieval_filters(
chunk,
RetrievalFilters(source_systems=["GITHUB"]),
)


def test_source_system_is_recognised_case_insensitively() -> None:
assert normalize_source_system("bitbucket") == "BITBUCKET"
assert is_source_system("BITBUCKET")
assert normalize_source_system("gitlab") is None
assert normalize_source_system(None) is None


def test_source_timestamp_preferred_over_indexed_at() -> None:
chunk = to_chunk(
ParsedChunk(
Expand Down
Loading