Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a91d6e1
Fix OWASP agent probe gaps for chapters, people, membership, and CRE …
northdpole Oct 2, 2026
6953269
Add synth v2 probe suite and raise agent routing pass rate.
northdpole Oct 3, 2026
3c6bcff
Format owasp_agent modules with black for CI lint.
northdpole Oct 3, 2026
6da242e
Document OWASP agent setup, OIE relationship, and synth eval.
northdpole Oct 3, 2026
6d9eb6b
Black-format remaining owasp_agent modules for CI.
northdpole Oct 3, 2026
060e651
Answer OWASP entity chat probes from agent metadata.
northdpole Oct 12, 2026
8ce9b6c
Add multi-turn chat memory with New chat.
northdpole Oct 12, 2026
32aefc1
Harden OWASP agent routing for board, geo, CRE, and PG index.
northdpole Oct 14, 2026
e75479a
Route OWASP chat via multi-lane embedding similarity.
northdpole Oct 14, 2026
a69c0d3
Make OWASP agent meta embedding work on low-memory staging.
northdpole Oct 14, 2026
aeb2ebf
Add adversarial completion asker and align embed model ids.
northdpole Oct 14, 2026
033597b
Fix OWASP agent routing for chapter-in-city and project leaders.
northdpole Oct 14, 2026
34907f9
Treat named-chapter meetup asks as chapter lookup.
northdpole Oct 14, 2026
7a1cf7d
Reuse one Flask app in the adversarial completion asker.
northdpole Oct 14, 2026
3772492
feat(owasp-agent): sim-first retrieve with normalize, chapters, board…
northdpole Oct 15, 2026
1202fa4
fix(owasp-agent): stop substring event/chapter traps and city hardcodes
northdpole Oct 15, 2026
5369337
fix(owasp-agent): drop meta/normative keyword dump gates
northdpole Oct 15, 2026
b32ebbf
fix(owasp-agent): drop keyword dumps; derive project aliases on ingest
northdpole Oct 15, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ standards_cache.sqlite
!docs/Mid_eval_blog_gsoc2026/module_B_mideval_blog.md
!docs/Final_eval_blog_gsoc2026/module_B_final_blog.md
!application/utils/librarian/README.md
!application/utils/owasp_agent/README.md
!docs/gsoc_2026_module_c/*.md
!docs/gsoc_2026_module_a/*.md
!docs/Final_eval_blog_gsoc2026/*.md
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.ONESHELL:

.PHONY: run test covers install-deps dev docker lint frontend clean all e2e e2e-db owasp-agent-sync owasp-agent-merge-concepts
.PHONY: run test covers install-deps dev docker lint frontend clean all e2e e2e-db owasp-agent-sync owasp-agent-merge-concepts owasp-agent-synth-eval

prod-run:
gunicorn cre:app --log-file=-
Expand Down Expand Up @@ -217,6 +217,11 @@ owasp-agent-merge-concepts:
[ -d "./venv" ] && . ./venv/bin/activate &&\
PYTHONPATH=. python scripts/owasp_agent_merge_concepts.py $(OWASP_AGENT_MERGE_ARGS)

# Score fixtures/owasp_agent_synth_v2.jsonl against OWASP_AGENT_DB (see application/utils/owasp_agent/README.md).
owasp-agent-synth-eval:
[ -d "./venv" ] && . ./venv/bin/activate &&\
OWASP_AGENT_ENABLED=1 PYTHONPATH=. python scripts/owasp_agent_synth_eval.py

openapi-generate:
[ -d "./venv" ] && . ./venv/bin/activate &&\
python scripts/generate_openapi.py
Expand Down
5 changes: 5 additions & 0 deletions application/database/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ class Embeddings(BaseModel): # type: ignore
# vectors (legacy CSV ``embeddings`` column is dropped in that migration).
# SQLite/tests: Text holding a pgvector literal ``[1.0,2.0,...]``.
embedding_vec = sqla.Column(sqla.Text, nullable=False)
# Multi-lane chat routing: cre_graph | corpus | owasp_agent_meta
# (Alembic c9d0e1f2a3b4). Nullable for legacy rows until backfilled.
source_channel = sqla.Column(sqla.String, nullable=True, default=None)


class GapAnalysisResults(BaseModel):
Expand Down Expand Up @@ -3112,6 +3115,7 @@ def add_embedding(
embedding_model_id=embedding_model_id,
embedding_dim=embedding_dim,
embedding_vec=embedding_vec_literal,
source_channel="cre_graph",
)
else:
emb = Embeddings(
Expand All @@ -3122,6 +3126,7 @@ def add_embedding(
embedding_model_id=embedding_model_id,
embedding_dim=embedding_dim,
embedding_vec=embedding_vec_literal,
source_channel="corpus",
)
self.session.add(emb)
self.session.commit()
Expand Down
14 changes: 10 additions & 4 deletions application/frontend/src/pages/chatbot/chatbot.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ h1.ui.header.chatbot-title {
margin-bottom: 1rem !important;
}

.chatbot-toolbar {
display: flex;
justify-content: center;
margin: 0 auto 0.5rem;
max-width: 960px;
}

/* Tablet & below */
@media (max-width: 768px) {
h1.ui.header.chatbot-title {
Expand Down Expand Up @@ -270,7 +277,7 @@ h1.ui.header.chatbot-title {

margin-top: 1rem;
background-color: #daebdb;
/* background: transparent; */
/* background: transparent; */
border: 1px solid rgb(190, 214, 232);
padding: 0.75rem;
border-radius: 12px;
Expand Down Expand Up @@ -399,7 +406,7 @@ h1.ui.header.chatbot-title {
flex-direction: column;
height: 100%;

background: #fcfcfa; /* same palette as input */
background: #fcfcfa; /* same palette as input */
backdrop-filter: blur(6px);

border-radius: 18px;
Expand Down Expand Up @@ -434,7 +441,6 @@ h1.ui.header.chatbot-title {
========================= */

@keyframes typingBounce {

0%,
80%,
100% {
Expand Down Expand Up @@ -470,4 +476,4 @@ h1.ui.header.chatbot-title {
opacity: 0.85;
transform: translateX(-50%) translateY(0);
}
}
}
25 changes: 24 additions & 1 deletion application/frontend/src/pages/chatbot/chatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ export const Chatbot = () => {
return res;
}

const HISTORY_CAP = 20;

function startNewChat() {
setChatMessages([]);
setError('');
setChat(DEFAULT_CHAT_STATE);
setModelName('');
setShowScrollToBottom(false);
}

async function onSubmit() {
if (!chat.term.trim()) return;
shouldForceScrollRef.current = true;
Expand All @@ -144,6 +154,12 @@ export const Chatbot = () => {
setChat({ ...chat, term: '' });
setLoading(true);

const prior = chatMessages.slice(-(HISTORY_CAP - 1)).map((m) => ({
role: m.role === 'assistant' ? 'assistant' : 'user',
content: m.message,
}));
const messages = [...prior, { role: 'user', content: currentTerm }];

setChatMessages((prev) => [
...prev,
{
Expand All @@ -158,7 +174,7 @@ export const Chatbot = () => {
fetch(`${apiUrl}/completion`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ prompt: currentTerm }),
body: JSON.stringify({ prompt: currentTerm, messages }),
})
.then(async (response) => {
if (!response.ok) {
Expand Down Expand Up @@ -272,6 +288,13 @@ export const Chatbot = () => {
<Header as="h1" className="chatbot-title">
OWASP OpenCRE Chat
</Header>
{hasMessages ? (
<div className="chatbot-toolbar">
<Button type="button" size="small" basic onClick={startNewChat}>
<Icon name="plus" /> New chat
</Button>
</div>
) : null}

<Container>
<div className={`chat-container ${hasMessages ? 'chat-active' : 'chat-landing'}`}>
Expand Down
2 changes: 1 addition & 1 deletion application/frontend/www/bundle.js

Large diffs are not rendered by default.

41 changes: 33 additions & 8 deletions application/prompt_client/prompt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,21 +954,31 @@ def _litellm_get_text_embeddings(
return vectors
return vectors[0]

def create_chat_completion(self, prompt: str, closest_object_str: str) -> str:
def create_chat_completion(
self,
prompt: str,
closest_object_str: str,
history: Optional[List[Dict[str, str]]] = None,
) -> str:
rag_instruction = _render_chat_prompt(
question=prompt,
retrieved_knowledge=closest_object_str,
)
messages = [
messages: List[Dict[str, str]] = [
{
"role": "system",
"content": (
"You are OpenCRE Chat, a cybersecurity assistant. "
"Follow the user instructions strictly."
),
},
{"role": "user", "content": rag_instruction},
]
for turn in history or []:
role = str(turn.get("role") or "").lower()
content = str(turn.get("content") or "").strip()
if role in ("user", "assistant") and content:
messages.append({"role": role, "content": content})
messages.append({"role": "user", "content": rag_instruction})

resp = litellm_router.completion(
model=self.chat_model,
Expand Down Expand Up @@ -1041,21 +1051,30 @@ def align_embedding_span_json(
)
raise

def query_llm(self, raw_question: str) -> str:
def query_llm(
self,
raw_question: str,
history: Optional[List[Dict[str, str]]] = None,
) -> str:
direct_instruction = _render_chat_prompt(
question=raw_question,
retrieved_knowledge=None,
)
messages = [
messages: List[Dict[str, str]] = [
{
"role": "system",
"content": (
"You are OpenCRE Chat, a cybersecurity assistant. "
"Follow the user instructions strictly."
),
},
{"role": "user", "content": direct_instruction},
]
for turn in history or []:
role = str(turn.get("role") or "").lower()
content = str(turn.get("content") or "").strip()
if role in ("user", "assistant") and content:
messages.append({"role": role, "content": content})
messages.append({"role": "user", "content": direct_instruction})

resp = litellm_router.completion(
model=self.chat_model,
Expand Down Expand Up @@ -1412,14 +1431,19 @@ def get_id_of_most_similar_node_paginated(
return None, None
return most_similar_id, max_similarity

def generate_text(self, prompt: str) -> Dict[str, str]:
def generate_text(
self,
prompt: str,
history: Optional[List[Dict[str, str]]] = None,
) -> Dict[str, str]:
"""
Generate text is a frontend method used for the chatbot
It matches the prompt/user question to an embedding from our database and then sends both the
text that generated the embedding and the user prompt to an llm for explaining

Args:
prompt (str): user question
history: prior chat turns as native ``[{role, content}]`` messages

Returns:
Dict[str,str]: a dictionary with the response and the closest object
Expand Down Expand Up @@ -1477,6 +1501,7 @@ def generate_text(self, prompt: str) -> Dict[str, str]:
answer = self.ai_client.create_chat_completion(
prompt=prompt,
closest_object_str=closest_object_str,
history=history,
)
accurate = True

Expand All @@ -1488,7 +1513,7 @@ def generate_text(self, prompt: str) -> Dict[str, str]:
row["embeddingsUrl"] = narrow
table.append(row)
else:
answer = self.ai_client.query_llm(prompt)
answer = self.ai_client.query_llm(prompt, history=history)

logger.debug(f"retrieved completion for {prompt}")
result = f"Answer: {answer}"
Expand Down
57 changes: 57 additions & 0 deletions application/tests/chat_completion_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,63 @@ def test_completion_propagates_unrelated_runtime_error(self) -> None:
content_type="application/json",
)

def test_completion_passes_messages_history_to_generate_text(self) -> None:
os.environ["NO_LOGIN"] = "1"
os.environ["OWASP_AGENT_ENABLED"] = "0"
with patch("application.prompt_client.prompt_client.PromptHandler") as mock_ph:
mock_ph.return_value.generate_text.return_value = {
"response": "Answer: ok",
"table": [],
"accurate": True,
"model_name": "test-llm",
}
with self.app.test_client() as client:
response = client.post(
"/rest/v1/completion",
json={
"prompt": "and in Morocco?",
"messages": [
{
"role": "user",
"content": "OWASP membership cost in Switzerland?",
},
{"role": "assistant", "content": "Answer: standard dues"},
{"role": "user", "content": "and in Morocco?"},
],
},
content_type="application/json",
)
self.assertEqual(200, response.status_code)
kwargs = mock_ph.return_value.generate_text.call_args
# positional prompt + history kw
self.assertEqual(kwargs.args[0], "and in Morocco?")
history = kwargs.kwargs.get("history")
self.assertIsNotNone(history)
assert history is not None
self.assertEqual(history[0]["role"], "user")
self.assertIn("Switzerland", history[0]["content"])
self.assertEqual(history[-1]["role"], "assistant")
os.environ.pop("OWASP_AGENT_ENABLED", None)


class TestChatHistoryHelpers(unittest.TestCase):
def test_split_prompt_strips_trailing_user_duplicate(self) -> None:
from application.utils.chat_history import split_prompt_and_history

prompt, history = split_prompt_and_history(
{
"prompt": "follow up",
"messages": [
{"role": "user", "content": "first"},
{"role": "assistant", "content": "Answer: hi"},
{"role": "user", "content": "follow up"},
],
}
)
self.assertEqual(prompt, "follow up")
self.assertEqual(len(history), 2)
self.assertEqual(history[0]["content"], "first")


if __name__ == "__main__":
unittest.main()
Loading
Loading