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
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ def handle(self, *args, **options) -> None:
project.invalid_urls.add(repository_url)
project.related_urls.remove(repository_url)
project.save(update_fields=("invalid_urls", "related_urls"))
continue
else:
logger.exception("Unexpected error fetching repository %s", repository_url)
continue

try:
organization, repository = sync_repository(gh_repository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def add_arguments(self, parser) -> None:
"--organization",
required=False,
type=str,
help="The organization name (e.g. juice-shop, DefectDojo')",
help="The organization name (e.g. juice-shop, DefectDojo)",
)

def handle(self, *_args, **options) -> None:
Expand Down
2 changes: 1 addition & 1 deletion backend/apps/github/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def normalize_url(url: str, *, check_path: bool = False) -> str | None:
http_prefix = "http://" # NOSONAR
https_prefix = "https://"
if not parsed_url.scheme:
url = f"{https_prefix}{url}"
url = f"{https_prefix}{url.lstrip('/')}"

normalized_url = (
f"{https_prefix}{url[len(http_prefix) :]}" if url.startswith(http_prefix) else url
Expand Down
26 changes: 26 additions & 0 deletions backend/tests/apps/ai/agent/nodes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,32 @@ def test_retrieve_skips_if_chunks_present(self, nodes):
new_state = nodes.retrieve(state)
assert new_state == state

def test_retrieve_with_existing_metadata(self, nodes, mocker):
"""Test retrieve when extracted_metadata already exists in state."""
state = {
"extracted_metadata": {
"entity_types": ["chapter"],
"filters": {},
"requested_fields": [],
},
"query": "test query",
}

nodes.retriever.retrieve.return_value = [{"text": "chunk1", "similarity": 0.9}]
nodes.filter_chunks_by_metadata = mocker.Mock(
return_value=[{"text": "chunk1", "similarity": 0.9}]
)

new_state = nodes.retrieve(state)

assert "context_chunks" in new_state
nodes.retriever.retrieve.assert_called_with(
content_types=["chapter"],
limit=DEFAULT_CHUNKS_RETRIEVAL_LIMIT,
query="test query",
similarity_threshold=DEFAULT_SIMILARITY_THRESHOLD,
)

def test_generate_logic(self, nodes):
state = {"query": "test query", "context_chunks": []}
nodes.generator.generate_answer.return_value = "Generated answer"
Expand Down
29 changes: 29 additions & 0 deletions backend/tests/apps/ai/agent/tools/rag/generator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,32 @@ def test_constants(self):
"""Test class constants have expected values."""
assert Generator.MAX_TOKENS == 2000
assert math.isclose(Generator.TEMPERATURE, 0.5)

def test_prepare_context_with_additional_context(self):
"""Test context preparation with chunks that have additional_context."""
with (
patch.dict(os.environ, {"DJANGO_OPEN_AI_SECRET_KEY": "test-key"}),
patch("openai.OpenAI"),
):
generator = Generator()

chunks = [
{
"source_name": "Chapter 1",
"text": "This is chapter 1 content",
"additional_context": {"location": "New York", "region": "North America"},
},
{
"source_name": "Chapter 2",
"text": "This is chapter 2 content",
"additional_context": {},
},
]

result = generator.prepare_context(chunks)

assert "Additional Context:" in result
assert "location" in result
assert "New York" in result
assert "Source Name: Chapter 1" in result
assert "Source Name: Chapter 2" in result
107 changes: 82 additions & 25 deletions backend/tests/apps/ai/agent/tools/rag/retriever_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,23 +336,16 @@ def test_get_additional_context_committee(self):
for key in expected_keys:
assert key in result

def test_get_additional_context_message(self):
"""Test getting additional context for message content type."""
def test_get_additional_context_message_with_conversation_but_no_attributes(self):
"""Test additional context for message with conversation lacking slack_channel_id."""
with (
patch.dict(os.environ, {"DJANGO_OPEN_AI_SECRET_KEY": "test-key"}),
patch("openai.OpenAI"),
):
retriever = Retriever()

conversation = MagicMock()
conversation.slack_channel_id = "C1234567890"

parent_message = MagicMock()
parent_message.ts = "1234567890.123456"

author = MagicMock()
author.name = "testuser"

conversation = MagicMock(spec=[])
parent_message = MagicMock(spec=[])
author = MagicMock(spec=[])
content_object = MagicMock()
content_object.__class__.__name__ = "Message"
content_object.conversation = conversation
Expand All @@ -362,17 +355,12 @@ def test_get_additional_context_message(self):

result = retriever.get_additional_context(content_object)

expected_keys = ["channel", "thread_ts", "ts", "user"]
for key in expected_keys:
assert key in result

assert result["channel"] == "C1234567890"
assert result["thread_ts"] == "1234567890.123456"
assert result["ts"] == "1234567891.123456"
assert result["user"] == "testuser"
assert "channel" not in result or result.get("channel") is None
assert "thread_ts" not in result or result.get("thread_ts") is None
assert "user" not in result or result.get("user") is None

def test_get_additional_context_message_no_conversation(self):
"""Test getting additional context for message with no conversation."""
def test_get_additional_context_message_with_falsy_conversation(self):
"""Test getting additional context for message when conversation evaluates to False."""
with (
patch.dict(os.environ, {"DJANGO_OPEN_AI_SECRET_KEY": "test-key"}),
patch("openai.OpenAI"),
Expand All @@ -381,14 +369,31 @@ def test_get_additional_context_message_no_conversation(self):

content_object = MagicMock()
content_object.__class__.__name__ = "Message"
content_object.conversation = None
content_object.parent_message = None
content_object.conversation = 0
content_object.parent_message = False
content_object.author = ""
content_object.ts = "1234567891.123456"
content_object.author = None

result = retriever.get_additional_context(content_object)

assert result["ts"] == "1234567891.123456"
for key in ["channel", "thread_ts", "user"]:
assert result.get(key) is None

def test_get_additional_context_unknown_content_type(self):
"""Test getting additional context for an unknown content type returns empty dict."""
with (
patch.dict(os.environ, {"DJANGO_OPEN_AI_SECRET_KEY": "test-key"}),
patch("openai.OpenAI"),
):
retriever = Retriever()

content_object = MagicMock()
content_object.__class__.__name__ = "User"

result = retriever.get_additional_context(content_object)

assert result == {}

def test_extract_content_types_from_query_single_type(self):
"""Test extracting single content type from query."""
Expand Down Expand Up @@ -593,3 +598,55 @@ def test_retrieve_with_none_content_object(self, mock_chunk, mock_logger):
mock_logger.warning.assert_called_once_with(
"Content object is None for chunk %s. Skipping.", 1
)

def test_get_additional_context_message_with_author(self):
"""Test getting additional context for message with named author."""
with (
patch.dict(os.environ, {"DJANGO_OPEN_AI_SECRET_KEY": "test-key"}),
patch("openai.OpenAI"),
):
retriever = Retriever()

conversation = MagicMock()
conversation.slack_channel_id = "C123456"

parent_message = MagicMock()
parent_message.ts = "1234567890.123456"

author = MagicMock()
author.name = "John Doe"

content_object = MagicMock()
content_object.__class__.__name__ = "Message"
content_object.conversation = conversation
content_object.parent_message = parent_message
content_object.ts = "1234567890.654321"
content_object.author = author

result = retriever.get_additional_context(content_object)

assert result["channel"] == "C123456"
assert result["thread_ts"] == "1234567890.123456"
assert result["ts"] == "1234567890.654321"
assert result["user"] == "John Doe"

def test_get_additional_context_message_none_fields(self):
"""Test getting additional context for message with None fields."""
with (
patch.dict(os.environ, {"DJANGO_OPEN_AI_SECRET_KEY": "test-key"}),
patch("openai.OpenAI"),
):
retriever = Retriever()

content_object = MagicMock()
content_object.__class__.__name__ = "Message"
content_object.conversation = None
content_object.parent_message = None
content_object.ts = "1234567890.654321"
content_object.author = None

result = retriever.get_additional_context(content_object)

assert result["ts"] == "1234567890.654321"
for key in ["channel", "thread_ts", "user"]:
assert result.get(key) is None
4 changes: 2 additions & 2 deletions backend/tests/apps/ai/common/base/ai_command_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def test_setup_openai_client_success(self, mock_openai_class, command):

result = command.setup_openai_client()

assert result is True
assert result
assert command.openai_client == mock_client
mock_openai_class.assert_called_once_with(api_key="test-api-key")

Expand All @@ -197,7 +197,7 @@ def test_setup_openai_client_no_api_key(self, command):
with patch.object(command.stdout, "write") as mock_write:
result = command.setup_openai_client()

assert result is False
assert not result
assert command.openai_client is None
mock_write.assert_called_once()
call_args = mock_write.call_args[0][0]
Expand Down
Loading