diff --git a/.gitignore b/.gitignore
index 14eea981f..07520ee9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/Makefile b/Makefile
index a4ff3026b..1fe72f80f 100644
--- a/Makefile
+++ b/Makefile
@@ -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=-
@@ -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
diff --git a/application/database/db.py b/application/database/db.py
index 22e7358be..db006da0d 100644
--- a/application/database/db.py
+++ b/application/database/db.py
@@ -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):
@@ -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(
@@ -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()
diff --git a/application/frontend/src/pages/chatbot/chatbot.scss b/application/frontend/src/pages/chatbot/chatbot.scss
index efd733b3a..582f786e3 100644
--- a/application/frontend/src/pages/chatbot/chatbot.scss
+++ b/application/frontend/src/pages/chatbot/chatbot.scss
@@ -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 {
@@ -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;
@@ -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;
@@ -434,7 +441,6 @@ h1.ui.header.chatbot-title {
========================= */
@keyframes typingBounce {
-
0%,
80%,
100% {
@@ -470,4 +476,4 @@ h1.ui.header.chatbot-title {
opacity: 0.85;
transform: translateX(-50%) translateY(0);
}
-}
\ No newline at end of file
+}
diff --git a/application/frontend/src/pages/chatbot/chatbot.tsx b/application/frontend/src/pages/chatbot/chatbot.tsx
index 4c8ae6afc..4d001469e 100644
--- a/application/frontend/src/pages/chatbot/chatbot.tsx
+++ b/application/frontend/src/pages/chatbot/chatbot.tsx
@@ -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;
@@ -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,
{
@@ -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) {
@@ -272,6 +288,13 @@ export const Chatbot = () => {