Skip to content

Accumulated changes to the integration branch - #1596

Open
ralf-berger wants to merge 539 commits into
mainfrom
dev
Open

Accumulated changes to the integration branch#1596
ralf-berger wants to merge 539 commits into
mainfrom
dev

Conversation

@ralf-berger

Copy link
Copy Markdown
Collaborator

Branching strategy: Everything gets merged into dev first, then merged again into main.

Nourhanbd and others added 30 commits February 17, 2025 23:11
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.26.8 to 7.26.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.9/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [mongoose](https://github.com/Automattic/mongoose) from 8.10.0 to 8.10.1.
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](Automattic/mongoose@8.10.0...8.10.1)

---
updated-dependencies:
- dependency-name: mongoose
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps nginxinc/nginx-unprivileged from 1.27.3-alpine to 1.27.4-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) from 5.1.5 to 5.1.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine)

---
updated-dependencies:
- dependency-name: "@types/jasmine"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [neo4j-driver](https://github.com/neo4j/neo4j-javascript-driver) from 5.28.0 to 5.28.1.
- [Release notes](https://github.com/neo4j/neo4j-javascript-driver/releases)
- [Commits](neo4j/neo4j-javascript-driver@5.28.0...5.28.1)

---
updated-dependencies:
- dependency-name: neo4j-driver
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps nginxinc/nginx-unprivileged from 1.27.3-alpine to 1.27.4-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…MKG"

courseId was undefined in the HTTP request.
Prepared the logging for the activity "User filtered top N concepts".
It doesn't work yet.
…w/ understood/ not understood.

-Fixed the problem of the activity logging of "User viewed recommended concepts"
-edited the type of the recommended concepts. Previously they could be either main_concept/ related_concept or both.
-Now the ones that are under recommended concepts Tab, they are considered as recommended_concept.
-Added type to the concepts while marking as understood/ not understood/ new.
-There is still a logging problem, by marking a concept from the recommended materials tab!
…mended concept"

-Additionally, commented out some unnecessary console logs and added comments where needed.
Two activites are being logged by adding an annotation:
"User added an annotation" and "User annotated a material".
The annotation object can be "Note", "Question", "External Resource".
The material object can be "pdf", "video", "Youtube".
So the 6 possibilities are the following:
"User added a note",
"User added an external resource",
"User asked a question",
"User annotated a PDF",
"User annotated a video",
"User annotated a youtube video".
The activities include "User zoomed in a pdf", "User zoomed out a pdf", "User reset zoom in a pdf"
-Added comments for the previous implementation
-Removed console logs.
The activity is "User viewed a Material's slide"
-Logged the Activity "User did not understand a slide", additionally to "User accessed Slide Kg"
Activities include: "User marked a notification as read",  "User marked a notification as unread"
fixing a typo and adding some text
Activities include:
"User viewed notifications "
"User marked a notification/s as read",
"User marked a notification/s as unread",
"User starred a notification/s",
"User unstarred a notification/s",
"User deleted a notification/s"
Activities include:
"User follow/ unfollow/ hid/ unhid/ filtered/ replied/ added/asked"
Implementation include the following activities:
"User accessed Slide Knowledge Graph",
"User accessed Material Knowledge Graph",
"User accessed Course Knowledge Graph",
"User did not understand a Slide".
postgres:16 image no longer requires Debian archive hacks

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
coursemapper-kg/wp-pg/Dockerfile (4)

15-15: Ensure the custom entrypoint is executable at build time

Make the permission explicit to avoid relying on host file mode.

Apply this diff:

-COPY bin/entrypoint.sh /usr/local/bin/custom-entrypoint.sh
+COPY --chmod=0755 bin/entrypoint.sh /usr/local/bin/custom-entrypoint.sh

18-18: Reconsider rclone config placement and ownership

Copying to /root likely won’t be readable if the process runs as postgres. Prefer a well-known location and proper ownership, e.g. /etc/rclone/rclone.conf (system-wide) or $PGUSER’s XDG config (/var/lib/postgresql/.config/rclone/rclone.conf).

Minimal, system-wide approach (requires the directory):

+# create system rclone config dir
+RUN install -d -m 0755 /etc/rclone
-COPY config/rclone.conf.orig /root
+COPY --chmod=0644 config/rclone.conf.orig /etc/rclone/rclone.conf

If you keep a per-user config instead, create the tree and set ownership:

RUN install -d -o postgres -g postgres -m 0700 /var/lib/postgresql/.config/rclone
COPY --chown=postgres:postgres --chmod=0600 config/rclone.conf.orig /var/lib/postgresql/.config/rclone/rclone.conf

20-20: Add a container HEALTHCHECK

Helps orchestration detect readiness/liveness; matches Checkov hint CKV_DOCKER_2.

Apply this diff:

 ENTRYPOINT ["/usr/local/bin/custom-entrypoint.sh"]
 
+HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=5 \
+  CMD pg_isready -U "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-postgres}" || exit 1

15-20: Run as non-root (or drop privileges) to satisfy least-privilege and Checkov CKV_DOCKER_3

If your entrypoint doesn’t need root, explicitly run as postgres. Otherwise, ensure the script drops privileges (e.g., exec gosu postgres …) before starting Postgres.

One-option diff (if root not required):

-COPY --chmod=0755 bin/entrypoint.sh /usr/local/bin/custom-entrypoint.sh
-ENTRYPOINT ["/usr/local/bin/custom-entrypoint.sh"]
+COPY --chmod=0755 bin/entrypoint.sh /usr/local/bin/custom-entrypoint.sh
+USER postgres
+ENTRYPOINT ["/usr/local/bin/custom-entrypoint.sh"]

If root is required during init, keep USER as root but drop privileges within the script before launching Postgres.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 43b537e and 322d644.

📒 Files selected for processing (1)
  • coursemapper-kg/wp-pg/Dockerfile (1 hunks)
🧰 Additional context used
🪛 Checkov (3.2.334)
coursemapper-kg/wp-pg/Dockerfile

[LOW] 1-20: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[LOW] 1-20: Ensure that a user for the container has been created

(CKV_DOCKER_3)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build container images / Build image (coursemapper-kg/preprocess)
  • GitHub Check: Build container images / Build image (coursemapper-kg/concept-map)
  • GitHub Check: Build container images / Build image (webapp)
🔇 Additional comments (2)
coursemapper-kg/wp-pg/Dockerfile (2)

1-1: Dockerfile syntax 1.15: good upgrade; ensure your builders default to BuildKit

Adopting 1.15 unlocks heredocs and cache mounts used below. Confirm your CI/builders run with DOCKER_BUILDKIT=1 (most do by default now).


15-20: Overriding Postgres ENTRYPOINT: verify you still initialize the cluster and drop privileges

Replacing the official docker-entrypoint.sh can bypass PG init scripts, env var handling (e.g., POSTGRES_DB/USER/PASSWORD), and the privilege drop to postgres. Ensure your custom-entrypoint.sh either:

  • chains to the official entrypoint (recommended), or
  • fully replicates its semantics and drops to the postgres user (via gosu) before starting the server.

If you intend to chain, a common pattern inside your script is:

exec /usr/local/bin/docker-entrypoint.sh "$@"

Optionally, if you prefer to run the container explicitly as a non-root user, add before ENTRYPOINT:

+USER postgres

Confirm your script does not require root if you set USER.


# Install dependencies
ENV RUNTIME_DEPS "ca-certificates rclone"
ENV RUNTIME_DEPS="ca-certificates rclone"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Use ARG for build-time deps to avoid leaking ENV into the runtime image

RUNTIME_DEPS is only needed during build; prefer ARG so it doesn’t become a runtime env var.

Apply this diff:

-ENV RUNTIME_DEPS="ca-certificates rclone"
+ARG RUNTIME_DEPS="ca-certificates rclone"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ENV RUNTIME_DEPS="ca-certificates rclone"
ARG RUNTIME_DEPS="ca-certificates rclone"
🤖 Prompt for AI Agents
In coursemapper-kg/wp-pg/Dockerfile around line 5, RUNTIME_DEPS is declared with
ENV which leaks build-only packages into the runtime image; change ENV
RUNTIME_DEPS="ca-certificates rclone" to an ARG declaration (e.g., ARG
BUILD_DEPS="ca-certificates rclone") and update subsequent references in the
Dockerfile to use that ARG during build steps (apt-get install, cleanup, etc.)
so the variable is not present as a runtime environment variable.

Comment on lines 6 to 12
RUN --mount=type=cache,sharing=private,target=/var/cache/apt \
--mount=type=cache,sharing=private,target=/var/lib/apt <<EOF
rm -f /etc/apt/apt.conf.d/docker-clean
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
DEBIAN_FRONTEND=noninteractive apt-get update -q &&
apt-get --reinstall install debian-archive-keyring &&
apt-get install -qq --no-install-recommends -o=Dpkg::Use-Pty=0 $RUNTIME_DEPS
EOF

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden the APT block: fail fast, noninteractive install, and don’t persist keep-cache config

  • Add set -eux to fail early.
  • Use -y for install (update doesn’t take -y).
  • Remove /etc/apt/apt.conf.d/keep-cache after install so the runtime image isn’t left with build-time APT behavior.
  • Optional: explicitly clear lists (mostly moot with cache mounts, but harmless).

Apply this diff:

 RUN --mount=type=cache,sharing=private,target=/var/cache/apt \
     --mount=type=cache,sharing=private,target=/var/lib/apt <<EOF
-  rm -f /etc/apt/apt.conf.d/docker-clean
-  echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
-  DEBIAN_FRONTEND=noninteractive apt-get update -q &&
-  apt-get install -qq --no-install-recommends -o=Dpkg::Use-Pty=0 $RUNTIME_DEPS
+  set -eux
+  rm -f /etc/apt/apt.conf.d/docker-clean
+  echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
+  DEBIAN_FRONTEND=noninteractive apt-get update -q
+  apt-get install -y -qq --no-install-recommends -o=Dpkg::Use-Pty=0 $RUNTIME_DEPS
+  rm -f /etc/apt/apt.conf.d/keep-cache
+  rm -rf /var/lib/apt/lists/*
 EOF
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RUN --mount=type=cache,sharing=private,target=/var/cache/apt \
--mount=type=cache,sharing=private,target=/var/lib/apt <<EOF
rm -f /etc/apt/apt.conf.d/docker-clean
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
DEBIAN_FRONTEND=noninteractive apt-get update -q &&
apt-get --reinstall install debian-archive-keyring &&
apt-get install -qq --no-install-recommends -o=Dpkg::Use-Pty=0 $RUNTIME_DEPS
EOF
RUN --mount=type=cache,sharing=private,target=/var/cache/apt \
--mount=type=cache,sharing=private,target=/var/lib/apt <<EOF
set -eux
rm -f /etc/apt/apt.conf.d/docker-clean
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
DEBIAN_FRONTEND=noninteractive apt-get update -q
apt-get install -y -qq --no-install-recommends -o=Dpkg::Use-Pty=0 $RUNTIME_DEPS
rm -f /etc/apt/apt.conf.d/keep-cache
rm -rf /var/lib/apt/lists/*
EOF
🤖 Prompt for AI Agents
In coursemapper-kg/wp-pg/Dockerfile around lines 6 to 12, the APT block should
be hardened: add a shell fail-fast and verbose header (set -eux) at the top of
the heredoc, use -y on apt-get install (DEBIAN_FRONTEND=noninteractive apt-get
install -y ... -qq --no-install-recommends -o=Dpkg::Use-Pty=0 $RUNTIME_DEPS),
and ensure we remove the temporary keep-cache file after installation (rm -f
/etc/apt/apt.conf.d/keep-cache) so runtime image isn’t left with build-time
behavior; optionally clear apt lists after install (rm -rf /var/lib/apt/lists/*)
to avoid persisting package lists (harmless with cache mounts).

ralf-berger and others added 2 commits August 27, 2025 10:10
postgres:16 image no longer requires Debian archive hacks

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (1)

1537-1566: Restore button state on error/complete of materials call

isRecommendationButtonDisplayed is reset only on success. Add resets on error/complete to avoid stuck UI.

           .subscribe({
             next: (result) => {
               this.isRecommendationButtonDisplayed = true;
               this.resourcesPagination = result;
               this.kgTabs.kgTabsEnable();
               this.mainConceptsTab = false;
               this.recommendedConceptsTab = false;
               this.recommendedMaterialsTab = true;
             },
-            complete: () => {
-              this.showRecommendationButtonClicked = false;
-            },
+            complete: () => {
+              this.isRecommendationButtonDisplayed = true;
+              this.showRecommendationButtonClicked = false;
+            },
-          });
+          }, 
+          error: (err) => {
+            this.isRecommendationButtonDisplayed = true;
+            this.showRecommendationButtonClicked = false;
+            console.error(err);
+            this.displayMessage(err.message);
+            this.isLoading = false;
+            this.loading.emit(false);
+          });
♻️ Duplicate comments (3)
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (3)

901-905: Don’t hard‑code graph width to 75%; compute responsively

Use container/sidebar widths instead of a magic percentage. This aligns with prior feedback.

-  setResponsiveWidthKnowledgeGraph(knowledgeGraph) {
-    if (knowledgeGraph && knowledgeGraph.style) {
-      knowledgeGraph.style.width = '75%';
-    }
-  }
+  setResponsiveWidthKnowledgeGraph(knowledgeGraph: HTMLElement | null) {
+    if (!knowledgeGraph) return;
+    const container = document.getElementById('slideKgDialogDiv') as HTMLElement | null;
+    const sidebar = document.getElementById('flexboxNotUnderstood') as HTMLElement | null;
+    const margin = this.showConceptsListSidebar ? 16 : 0; // 1rem
+    const containerWidth =
+      container?.offsetWidth ?? knowledgeGraph.parentElement?.clientWidth ?? window.innerWidth;
+    const sidebarWidth = this.showConceptsListSidebar && sidebar ? sidebar.offsetWidth : 0;
+    knowledgeGraph.style.width = Math.max(0, containerWidth - sidebarWidth - margin) + 'px';
+  }

2295-2298: Make croUpdater robust (early return + defaults)

Avoid silent no‑ops and undefined param flows. Mirrors earlier guidance to guard croComponent.

-  croUpdater(didNotUnderstandConceptsObj: any[], previousConceptsObj: any[]) {
-    this.croComponent?.updateCROformAll(didNotUnderstandConceptsObj, previousConceptsObj);
-  }
+  croUpdater(didNotUnderstandConceptsObj: any[] = [], previousConceptsObj: any[] = []) {
+    if (!this.croComponent) return;
+    this.croComponent.updateCROformAll(didNotUnderstandConceptsObj, previousConceptsObj);
+  }

2308-2324: Remove console noise and avoid event.screen; delegate to responsive helper

Use window.innerWidth and reuse setResponsiveWidthKnowledgeGraph. Also drop magic 40em.

-  setWeightGraphComponent(event) {
+  setWeightGraphComponent(event: UIEvent) {
     setTimeout(() => {
-      let knowledgeGraph = document.getElementById('graphSection');
-      if (this.showMaterialKg) {
-        console.warn("resize -> hideConceptsList -> HostListener event.screen ->", event.screen.width);
-        // let screenWidth = window.innerHeight;
-        let screenWidth = event.screen.width; // event.outerWidth
-
-        if (screenWidth >= 768 && screenWidth < 992) {
-          knowledgeGraph.style.width = '40em';
-        }
-        if (screenWidth > 992 && screenWidth <= 1200) {
-          knowledgeGraph.style.width = '40em';
-        }
-      }
+      const knowledgeGraph = document.getElementById('graphSection') as HTMLElement | null;
+      if (!knowledgeGraph || !this.showMaterialKg) return;
+      const screenWidth = window.innerWidth;
+      if (screenWidth >= 768 && screenWidth < 1200) {
+        this.setResponsiveWidthKnowledgeGraph(knowledgeGraph);
+      }
     }, 3);
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 322d644 and e35ebf1.

📒 Files selected for processing (1)
  • webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (25 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (1)
webapp/src/app/models/croForm.ts (2)
  • ActivatorPartCRO (4-8)
  • ResourcesPagination (66-73)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build container images / Build image (coursemapper-kg/preprocess)
  • GitHub Check: Build container images / Build image (coursemapper-kg/concept-map)
  • GitHub Check: Build container images / Build image (webapp)
  • GitHub Check: Build container images / Build image (coursemapper-kg/recommendation)
🔇 Additional comments (1)
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (1)

1490-1490: Verify necessity of PII in recommender requests

Before shipping, please confirm whether the userId (and user_id) fields you’re appending to the concept- and material-recommender payloads are actually consumed by the backend. If they’re not used server-side, remove them to avoid leaking unnecessary PII.

• webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (line 1489)

reqDataMaterial1["userId"] = this.userid;

• webapp/src/app/pages/components/knowledge-graph/result-view/result-view.component.ts (lines 321 & 342)

this.filteringParamsSavedTab.user_id = this.userId;

• webapp/src/app/pages/components/knowledge-graph/custom-recommendation-option/custom-recommendation-option.component.ts (line 141)

this.croForm["user_id"] = this.userId;

Comment on lines +272 to +274
resourcesPagination: ResourcesPagination = undefined;
isRecommendationButtonDisplayed = true;
conceptsUpdatedCRO: any;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Prefer explicit nullable type for resourcesPagination

Assigning undefined to a non‑nullable type weakens type safety.

-  resourcesPagination: ResourcesPagination = undefined;
+  resourcesPagination: ResourcesPagination | null = null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resourcesPagination: ResourcesPagination = undefined;
isRecommendationButtonDisplayed = true;
conceptsUpdatedCRO: any;
resourcesPagination: ResourcesPagination | null = null;
isRecommendationButtonDisplayed = true;
conceptsUpdatedCRO: any;
🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts
around lines 272 to 274, the field resourcesPagination is currently declared as
ResourcesPagination and assigned undefined which weakens type safety; change its
declaration to an explicit nullable type (ResourcesPagination | undefined) or
ResourcesPagination | null and keep the undefined assignment (or initialize to
null) so the compiler understands the field may be absent and enforces checks
where it is used.

Comment on lines +810 to +819
onActiveItemChange(event: MenuItem) {
// console.warn("tab onActiveItemChange") // graphSection
if (event.label === 'Main Concepts') {
// console.warn("Main Concepts")
} else if (event.label === 'Recommended Concepts') {
// console.warn("Recommended Concepts")
} else {
}

}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Clean up no‑op handler and commented logs

Either implement tab‑change side effects or keep a minimal no‑op.

-  onActiveItemChange(event: MenuItem) {
-    // console.warn("tab onActiveItemChange") // graphSection
-    if (event.label === 'Main Concepts') {
-      // console.warn("Main Concepts")
-    } else if (event.label === 'Recommended Concepts') {
-      // console.warn("Recommended Concepts")
-    } else {
-    }
-
-  }
+  onActiveItemChange(_event: MenuItem): void {
+    // No-op for now.
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onActiveItemChange(event: MenuItem) {
// console.warn("tab onActiveItemChange") // graphSection
if (event.label === 'Main Concepts') {
// console.warn("Main Concepts")
} else if (event.label === 'Recommended Concepts') {
// console.warn("Recommended Concepts")
} else {
}
}
onActiveItemChange(_event: MenuItem): void {
// No-op for now.
}
🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts
around lines 810 to 819, the onActiveItemChange handler contains only commented
console logs and empty conditional branches; remove the commented logs and the
empty else branch and either implement the needed tab-change side effects or
convert the function to a minimal no-op that clearly does nothing (e.g., keep a
single-line comment like "// no-op: tab changes handled elsewhere" and return),
ensuring the method is concise and has no unused branches or stray commented
code.

Comment on lines +1478 to 1479
this.tabIndex = 2;
this.showRecommendationButtonClicked = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix tab index: index 2 doesn’t exist with current tabs

Only two tabs remain (“Main Concepts”, “Recommended Materials”). Selecting index 2 is out of bounds and may misroute UI state.

-      this.tabIndex = 2;
+      this.tabIndex = 1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.tabIndex = 2;
this.showRecommendationButtonClicked = true;
this.tabIndex = 1;
this.showRecommendationButtonClicked = true;
🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts
around lines 1478 to 1479, the code sets this.tabIndex = 2 which is out of range
because only two tabs exist; update the assignment to use a valid index (set to
1 for the "Recommended Materials" tab) or compute it dynamically (e.g.
this.tabIndex = Math.min(desiredIndex, this.tabs.length - 1)) so tabIndex cannot
exceed available tabs; keep this.showRecommendationButtonClicked = true as-is.

Comment on lines +1535 to +1536
let reqDataFinal = this.croComponent.buildFinalRequestRecMaterial(reqData);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Null‑guard CRO before building request (fallback to reqData)

croComponent may be undefined; avoid runtime errors.

-          let reqDataFinal = this.croComponent.buildFinalRequestRecMaterial(reqData);
+          const reqDataFinal = this.croComponent
+            ? this.croComponent.buildFinalRequestRecMaterial(reqData)
+            : reqData;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let reqDataFinal = this.croComponent.buildFinalRequestRecMaterial(reqData);
const reqDataFinal = this.croComponent
? this.croComponent.buildFinalRequestRecMaterial(reqData)
: reqData;
🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts
around lines 1535-1536, the code calls
this.croComponent.buildFinalRequestRecMaterial(reqData) without guarding against
croComponent being undefined; add a null-guard so you only call
buildFinalRequestRecMaterial when croComponent exists and otherwise fall back to
using reqData (e.g. compute reqDataFinal = this.croComponent ?
this.croComponent.buildFinalRequestRecMaterial(reqData) : reqData), ensuring
types are adjusted or cast if necessary.

Comment on lines +2299 to +2306
setHeightGraphComponent() {
let knowledgeGraph = document.getElementById('graphSection');
if (knowledgeGraph) {
let ipo_interact = document.getElementById('ipo_interact');
// console.warn("ipo_interact with -> ", ipo_interact.offsetWidth)
this.cyHeight = ipo_interact.offsetHeight - (ipo_interact.offsetHeight * 0.15);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Null‑guard DOM node before using offsetHeight

ipo_interact may be null; prevent NPE.

-  setHeightGraphComponent() {
-    let knowledgeGraph = document.getElementById('graphSection');
-    if (knowledgeGraph) {
-      let ipo_interact = document.getElementById('ipo_interact');
-      // console.warn("ipo_interact with -> ", ipo_interact.offsetWidth)
-      this.cyHeight = ipo_interact.offsetHeight - (ipo_interact.offsetHeight * 0.15);
-    }
-  }
+  setHeightGraphComponent() {
+    const knowledgeGraph = document.getElementById('graphSection');
+    if (!knowledgeGraph) return;
+    const container = document.getElementById('ipo_interact') as HTMLElement | null;
+    if (container) {
+      this.cyHeight = container.offsetHeight * 0.85;
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setHeightGraphComponent() {
let knowledgeGraph = document.getElementById('graphSection');
if (knowledgeGraph) {
let ipo_interact = document.getElementById('ipo_interact');
// console.warn("ipo_interact with -> ", ipo_interact.offsetWidth)
this.cyHeight = ipo_interact.offsetHeight - (ipo_interact.offsetHeight * 0.15);
}
}
setHeightGraphComponent() {
const knowledgeGraph = document.getElementById('graphSection');
if (!knowledgeGraph) return;
const container = document.getElementById('ipo_interact') as HTMLElement | null;
if (container) {
this.cyHeight = container.offsetHeight * 0.85;
}
}
🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts
around lines 2299 to 2306, the code uses ipo_interact.offsetHeight without
guarding against ipo_interact being null; update the method to first check that
ipo_interact exists (e.g., if (ipo_interact) { ... }) before accessing
offsetHeight, and handle the null case by either returning early or using a safe
fallback height value so no NPE occurs.

* nber recommendations and search box activated

* support for large screen

---------

Co-authored-by: boby024 <williamkana46@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 27

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py (3)

373-383: User fetch/creation bugs: incorrect return handling and wrong function call signature.

  • get_user uses .single() then list(result): raises when result is None and returns meaningless list otherwise.
  • get_or_create_user calls create_user(tx, user_id, username, user_email), but create_user accepts only (tx, user_id).
-def get_user(tx, uid):
+def get_user(tx, uid):
@@
-    result = tx.run(
-        "MATCH (u:User) WHERE u.uid = $uid RETURN u",
-        uid=uid).single()
-
-    return list(result)
+    record = tx.run(
+        "MATCH (u:User) WHERE u.uid = $uid RETURN u.uid AS uid",
+        uid=uid).single()
+    return record["uid"] if record else None
 def get_or_create_user(self, user_id, username="", user_email=""):
@@
-            _uid = get_user(tx, user_id)
-            if _uid:
-                return _uid
-            else:
-                # print(tx)
-                create_user(tx, user_id, username, user_email)
-                tx.commit()
+            uid = get_user(tx, user_id)
+            if uid:
+                tx.commit()
+                return uid
+            create_user(tx, user_id)
+            tx.commit()

Also applies to: 749-768


716-725: Wrong argument order when creating user–concept relationship.

create_user_concept_relationships expects (tx, uid, cid, relation_type), but you pass (tx, concept_id, user_id, ...).

-                create_user_concept_relationships(tx, concept_id, user_id, relation_type)
+                create_user_concept_relationships(tx, user_id, concept_id, relation_type)

1780-1788: Invalid session.commit() usage; run in explicit transaction or use context manager.

Session has no commit(). Use a transaction or implicit auto-commit via session.run.

-        session = self.driver.session()
-        session.run(
-            """Match (c:Concept) WHERE c.cid=$cid and c.mid=$mid SET c.rank=$rank """,
-            cid=node["id"],
-            mid=node["mid"],
-            rank=node["rank"])
-        session.commit()
-        session.close()
+        with self.driver.session() as session:
+            session.run(
+                """MATCH (c:Concept) WHERE c.cid=$cid AND c.mid=$mid SET c.rank=$rank""",
+                cid=node["id"],
+                mid=node["mid"],
+                rank=node["rank"],
+            )
♻️ Duplicate comments (13)
webapp/src/app/pages/components/knowledge-graph/videos/card-video-list/card-video-list.component.html (1)

3-14: Add trackBy and remove the wrapper div to cut DOM churn

Move *ngFor onto the card component and provide trackBy for efficient re-rendering.

-      <div class="" *ngFor="let videoElement of videoElements">
-        <app-card-video 
+        <app-card-video
+          *ngFor="let videoElement of videoElements; trackBy: trackByRid"
           [notUnderstoodConcepts]="notUnderstoodConcepts" 
           (onWatchVideo)="readVideo($event)" 
           [videoElement]="videoElement" 
           [userId]="userId"
           [resultTabType]="resultTabType"
           (resourceRemovedEvent)="onResourceRemovedEvent($event)"
           [currentMaterial]="currentMaterial"
         >
         </app-card-video>
-      </div>

Note: Implement trackByRid(_: number, el: VideoElementModel) { return el.rid; } in the component TS.

webapp/src/app/pages/components/knowledge-graph/result-view/result-view.component.html (3)

57-60: Duplicate id "cro_sorting" on siblings.

Keep ids unique or remove them if not needed. This was flagged earlier as well.

-          <div id="cro_sorting" class="w-3/12"></div>
+          <div class="w-3/12"></div>
           
-          <div id="cro_sorting" class="w-3/12" (click)="op.toggle($event, targetEl)">
+          <div id="cro_sorting" class="w-3/12" (click)="op.toggle($event, targetEl)">

116-135: *Fix Angular ngIf microsyntax and render logic for Videos.

  • Use semicolon in microsyntax (previously reported).
  • Ensure the list renders only when items exist; otherwise show empty/loading states.
-        <div class="mt-1 w-full" id="cro_viedo_list">
-          <div
-            *ngIf="resourcesPagination?.nodes?.videos?.total_items > 0, else elseBlockRsVideos"
-          >
-          </div>
-            <p-scrollPanel styleClass="scroll_panel_list">
-              <app-card-video-list
-                [notUnderstoodConcepts]="concepts"
-                [videoElements]="resourcesPagination?.nodes?.videos?.content"
-                [userId]="userId"
-                (backButtonClicked)="logUserViewedRecommendedVideos()"
-                [currentMaterial]="currentMaterial"
-              ></app-card-video-list>
-            </p-scrollPanel>
-        </div>
+        <div class="mt-1 w-full">
+          <ng-container *ngIf="resourcesPagination?.nodes?.videos?.total_items > 0; else elseBlockRsVideos">
+            <p-scrollPanel styleClass="scroll_panel_list">
+              <app-card-video-list
+                [notUnderstoodConcepts]="concepts"
+                [videoElements]="resourcesPagination?.nodes?.videos?.content"
+                [userId]="userId"
+                (backButtonClicked)="logUserViewedRecommendedVideos()"
+                [currentMaterial]="currentMaterial"
+              ></app-card-video-list>
+            </p-scrollPanel>
+          </ng-container>
+        </div>

Also applies to: 136-156


159-175: *Fix Angular ngIf microsyntax and render logic for Articles.

Mirror the Videos fix; also remove duplicate/typo id.

-      <div class="mt-1 w-full" id="cro_viedo_list">
-        <div
-          *ngIf="resourcesPagination?.nodes?.articles?.total_items > 0, else elseBlockRsArticles"
-        >
-        </div>
-          <p-scrollPanel styleClass="scroll_panel_list">
-            <app-card-article-list
-              [notUnderstoodConcepts]="concepts"
-              [articleElements]="resourcesPagination?.nodes?.articles?.content"
-              [userId]="userId"
-              [currentMaterial]="currentMaterial"
-            ></app-card-article-list>
-          </p-scrollPanel>
-      </div>
+      <div class="mt-1 w-full">
+        <ng-container *ngIf="resourcesPagination?.nodes?.articles?.total_items > 0; else elseBlockRsArticles">
+          <p-scrollPanel styleClass="scroll_panel_list">
+            <app-card-article-list
+              [notUnderstoodConcepts]="concepts"
+              [articleElements]="resourcesPagination?.nodes?.articles?.content"
+              [userId]="userId"
+              [currentMaterial]="currentMaterial"
+            ></app-card-article-list>
+          </p-scrollPanel>
+        </ng-container>
+      </div>

Also applies to: 176-196

webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (8)

911-913: Stop manually setting width/margin; use the helper

Inline writes fight the helper and cause inconsistent layouts.

-      knowledgeGraph.style.marginLeft = 0 + 'rem';
-      knowledgeGraph.style.width = slideKgDialogDiv.offsetWidth + 'px';
+      this.setResponsiveWidthKnowledgeGraph();

270-275: Fix resourcesPagination nullability (assigned undefined/null to non-nullable type)

Make the type explicitly nullable and initialize consistently.

-  resourcesPagination: ResourcesPagination = undefined;
+  resourcesPagination: ResourcesPagination | null = null;

(No further code changes needed where you already set it to null in Line 1450.)

Also applies to: 1450-1451


810-819: Trim no-op tab-change handler

Convert to a minimal no-op; remove commented logs and empty branches.

-  onActiveItemChange(event: MenuItem) {
-    // console.warn("tab onActiveItemChange") // graphSection
-    if (event.label === 'Main Concepts') {
-      // console.warn("Main Concepts")
-    } else if (event.label === 'Recommended Concepts') {
-      // console.warn("Recommended Concepts")
-    } else {
-    }
-
-  }
+  onActiveItemChange(_event: MenuItem): void {
+    // No-op (tab side effects handled elsewhere)
+  }

843-853: Avoid hard-coded widths; compute width from container/sidebar; remove debug log

75%/85% breaks responsiveness and diverges from sidebar sizing. Compute based on container and sidebar widths.

-  setResponsiveWidthKnowledgeGraph() {
-    console.warn("window.innerWidth ", window.innerWidth)
-    let knowledgeGraph = document.getElementById('graphSection');
-    if (knowledgeGraph && knowledgeGraph.style) {
-      if (window.innerWidth < 2700) {
-        knowledgeGraph.style.width = '75%';
-      } else if (window.innerWidth > 2700) {
-        knowledgeGraph.style.width = '85%';
-      }
-    }
-  }
+  setResponsiveWidthKnowledgeGraph(): void {
+    const graph = document.getElementById('graphSection') as HTMLElement | null;
+    const container = document.getElementById('slideKgDialogDiv') as HTMLElement | null;
+    const sidebar = document.getElementById('flexboxNotUnderstood') as HTMLElement | null;
+    if (!graph || !container) return;
+    const sidebarWidth = this.showConceptsListSidebar && sidebar ? sidebar.offsetWidth : 0;
+    const padding = 16; // 1rem margin when sidebar is visible
+    const width = Math.max(320, container.offsetWidth - sidebarWidth - (this.showConceptsListSidebar ? padding : 0));
+    graph.style.marginLeft = this.showConceptsListSidebar ? '1rem' : '0';
+    graph.style.width = `${width}px`;
+  }

893-901: Unify spacing with the helper; drop inline marginLeft write

Delegate width/margin to setResponsiveWidthKnowledgeGraph to prevent drift.

-          if (knowledgeGraph) {
-            knowledgeGraph.style.marginLeft = 1 + 'rem';
-          }
+          // spacing handled in setResponsiveWidthKnowledgeGraph()

1487-1488: Fix tab index (2 is out of bounds for two tabs)

Set to 1 for “Recommended Materials”.

-      this.tabIndex = 2;
+      this.tabIndex = 1;

1542-1545: Null‑guard croComponent before building request (fallback to reqData)

Avoid runtime error when the child component isn’t initialized.

-          let reqDataFinal = this.croComponent.buildFinalRequestRecMaterial(reqData);
+          const reqDataFinal = this.croComponent
+            ? this.croComponent.buildFinalRequestRecMaterial(reqData)
+            : reqData;

2308-2315: Null‑guard ipo_interact before using offsetHeight

Prevents NPE when the container is missing on first render.

-  setHeightGraphComponent() {
-    let knowledgeGraph = document.getElementById('graphSection');
-    if (knowledgeGraph) {
-      let ipo_interact = document.getElementById('ipo_interact');
-      // console.warn("ipo_interact with -> ", ipo_interact.offsetWidth)
-      this.cyHeight = ipo_interact.offsetHeight - (ipo_interact.offsetHeight * 0.15);
-    }
-  }
+  setHeightGraphComponent(): void {
+    const container = document.getElementById('ipo_interact') as HTMLElement | null;
+    if (!container) return;
+    this.cyHeight = container.offsetHeight * 0.85;
+  }
coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py (1)

6-6: Remove unused import (duplicate of prior review).

import json isn’t used. Please delete it.

- import json
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e35ebf1 and 63591bd.

📒 Files selected for processing (4)
  • coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py (12 hunks)
  • webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (26 hunks)
  • webapp/src/app/pages/components/knowledge-graph/result-view/result-view.component.html (1 hunks)
  • webapp/src/app/pages/components/knowledge-graph/videos/card-video-list/card-video-list.component.html (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py (2)
webserver/src/graph/recommendation.neo4j.js (15)
  • result (27-27)
  • result (79-79)
  • result (186-192)
  • result (204-204)
  • result (307-307)
  • result (323-323)
  • result (344-344)
  • result (363-363)
  • resources (237-263)
  • query (206-227)
  • query (283-287)
  • query (308-312)
  • query (325-331)
  • query (345-350)
  • query (364-370)
webserver/src/controllers/recommendation.controller.js (12)
  • result (9-9)
  • result (20-20)
  • result (34-34)
  • result (45-45)
  • result (90-90)
  • result (104-104)
  • result (116-116)
  • result (129-129)
  • data (7-7)
  • data (18-18)
  • data (32-32)
  • cids (86-86)
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (1)
webapp/src/app/models/croForm.ts (2)
  • ActivatorPartCRO (4-8)
  • ResourcesPagination (66-73)
🪛 Ruff (0.12.2)
coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py

6-6: json imported but unused

Remove unused import: json

(F401)


129-129: Missing return type annotation for public function create_external_source_resource

Add return type annotation: None

(ANN201)


129-129: Missing type annotation for function argument tx

(ANN001)


129-129: Missing type annotation for function argument node

(ANN001)


134-134: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


134-134: Logging statement uses %

(G002)


493-493: Missing return type annotation for public function create_user_v2

(ANN201)


493-493: Missing type annotation for function argument tx

(ANN001)


493-493: Missing type annotation for function argument user

(ANN001)


503-503: Unnecessary assignment to user_id before return statement

Remove unnecessary assignment

(RET504)


1795-1795: Missing return type annotation for public function get_or_create_user_v2

(ANN201)


1795-1795: Missing type annotation for function argument user

(ANN001)


1816-1816: Missing return type annotation for public function create_or_update_video_resource

Add return type annotation: None

(ANN201)


1816-1816: Missing type annotation for function argument tx

(ANN001)


1817-1817: Missing type annotation for function argument recommendation_type

(ANN001)


1817-1817: Unused method argument: recommendation_type

(ARG002)


1818-1818: Boolean default positional argument in function definition

(FBT002)


1818-1818: Missing type annotation for function argument update_embedding_values

(ANN001)


1819-1819: Boolean default positional argument in function definition

(FBT002)


1819-1819: Missing type annotation for function argument update_detail_found

(ANN001)


1819-1819: Unused method argument: update_detail_found

(ARG002)


1855-1855: Avoid equality comparisons to True; use update_embedding_values: for truth checks

Replace with update_embedding_values

(E712)


1865-1865: Use node.get("keyphrases", []) instead of an if block

Replace with node.get("keyphrases", [])

(SIM401)


1866-1866: Use node.get("keyphrase_embedding", []) instead of an if block

Replace with node.get("keyphrase_embedding", [])

(SIM401)


1867-1867: Use node.get("document_embedding", []) instead of an if block

Replace with node.get("document_embedding", [])

(SIM401)


1868-1868: Use node.get("keyphrases_infos", "") instead of an if block

Replace with node.get("keyphrases_infos", "")

(SIM401)


1889-1889: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1893-1893: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1899-1899: Use node.get("keyphrases", []) instead of an if block

Replace with node.get("keyphrases", [])

(SIM401)


1900-1900: Use node.get("keyphrase_embedding", []) instead of an if block

Replace with node.get("keyphrase_embedding", [])

(SIM401)


1901-1901: Use node.get("document_embedding", []) instead of an if block

Replace with node.get("document_embedding", [])

(SIM401)


1902-1902: Use node.get("helpful_count", 0) instead of an if block

Replace with node.get("helpful_count", 0)

(SIM401)


1903-1903: Use node.get("not_helpful_count", 0) instead of an if block

Replace with node.get("not_helpful_count", 0)

(SIM401)


1904-1904: Use node.get("saves_count", 0) instead of an if block

Replace with node.get("saves_count", 0)

(SIM401)


1907-1907: datetime.datetime.now() called without a tz argument

(DTZ005)


1908-1908: Use node.get("keyphrases_infos", "") instead of an if block

Replace with node.get("keyphrases_infos", "")

(SIM401)


1910-1910: Do not catch blind exception: Exception

(BLE001)


1912-1912: Unnecessary pass statement

Remove unnecessary pass

(PIE790)


1914-1914: Missing return type annotation for public function create_or_update_wikipedia_resource

Add return type annotation: None

(ANN201)


1914-1914: Missing type annotation for function argument tx

(ANN001)


1914-1914: Missing type annotation for function argument node

(ANN001)


1914-1914: Missing type annotation for function argument recommendation_type

(ANN001)


1914-1914: Unused method argument: recommendation_type

(ARG002)


1915-1915: Boolean default positional argument in function definition

(FBT002)


1915-1915: Missing type annotation for function argument update_embedding_values

(ANN001)


1916-1916: Boolean default positional argument in function definition

(FBT002)


1916-1916: Missing type annotation for function argument update_detail_found

(ANN001)


1916-1916: Unused method argument: update_detail_found

(ARG002)


1944-1944: Avoid equality comparisons to True; use update_embedding_values: for truth checks

Replace with update_embedding_values

(E712)


1953-1953: Use node.get("keyphrases", []) instead of an if block

Replace with node.get("keyphrases", [])

(SIM401)


1954-1954: Use node.get("keyphrase_embedding", []) instead of an if block

Replace with node.get("keyphrase_embedding", [])

(SIM401)


1955-1955: Use node.get("document_embedding", []) instead of an if block

Replace with node.get("document_embedding", [])

(SIM401)


1956-1956: Use node.get("keyphrases_infos", "") instead of an if block

Replace with node.get("keyphrases_infos", "")

(SIM401)


1976-1976: Use node.get("keyphrases", []) instead of an if block

Replace with node.get("keyphrases", [])

(SIM401)


1979-1979: Use node.get("keyphrase_embedding", []) instead of an if block

Replace with node.get("keyphrase_embedding", [])

(SIM401)


1980-1980: Use node.get("document_embedding", []) instead of an if block

Replace with node.get("document_embedding", [])

(SIM401)


1981-1981: Use node.get("helpful_count", 0) instead of an if block

Replace with node.get("helpful_count", 0)

(SIM401)


1982-1982: Use node.get("not_helpful_count", 0) instead of an if block

Replace with node.get("not_helpful_count", 0)

(SIM401)


1983-1983: Use node.get("saves_count", 0) instead of an if block

Replace with node.get("saves_count", 0)

(SIM401)


1984-1984: datetime.datetime.now() called without a tz argument

(DTZ005)


1985-1985: Use node.get("keyphrases_infos", "") instead of an if block

Replace with node.get("keyphrases_infos", "")

(SIM401)


1987-1987: Do not catch blind exception: Exception

(BLE001)


1989-1989: Unnecessary pass statement

Remove unnecessary pass

(PIE790)


1991-1991: Missing return type annotation for public function get_top_n_concept_by_slide_id

(ANN201)


1991-1991: PEP 484 prohibits implicit Optional

Convert to Optional[T]

(RUF013)


1991-1991: Missing type annotation for function argument top_n

(ANN001)


2026-2026: Missing return type annotation for public function create_concept_modified

(ANN201)


2055-2094: Missing explicit return at the end of function able to return non-None value

Add explicit return statement

(RET503)


2055-2055: Missing return type annotation for public function update_rs_btw_user_and_cm

(ANN201)


2055-2055: Boolean default positional argument in function definition

(FBT002)


2055-2055: Missing type annotation for function argument only_status

(ANN001)


2062-2062: Avoid equality comparisons to True; use only_status: for truth checks

Replace with only_status

(E712)


2094-2094: Unnecessary assignment to r_detail before return statement

Remove unnecessary assignment

(RET504)


2096-2096: Missing return type annotation for public function update_rs_btw_user_and_cms

Add return type annotation: None

(ANN201)


2096-2096: Missing type annotation for function argument special_status

(ANN001)


2096-2096: Unused method argument: special_status

(ARG002)


2104-2104: Local variable result is assigned to but never used

Remove assignment to unused variable result

(F841)


2114-2114: Missing return type annotation for public function get_user_embedding_with_concept_modified

(ANN201)


2167-2167: Missing return type annotation for public function user_rates_resources

(ANN201)


2183-2183: Comparison to None should be cond is not None

Replace with cond is not None

(E711)


2183-2183: Avoid equality comparisons to True; use rating["reset"]: for truth checks

Replace with rating["reset"]

(E712)


2267-2267: Unnecessary assignment to result before return statement

Remove unnecessary assignment

(RET504)


2269-2269: Missing return type annotation for public function update_rs_btw_resource_and_cm

Add return type annotation: None

(ANN201)


2269-2269: Boolean default positional argument in function definition

(FBT002)


2269-2269: Missing type annotation for function argument action

(ANN001)


2300-2300: Do not catch blind exception: Exception

(BLE001)


2303-2303: Unnecessary pass statement

Remove unnecessary pass

(PIE790)


2305-2305: Missing return type annotation for public function update_rs_btw_resources_and_cm

Add return type annotation: None

(ANN201)


2305-2305: Boolean default positional argument in function definition

(FBT002)


2305-2305: Missing type annotation for function argument action

(ANN001)


2328-2328: Do not catch blind exception: Exception

(BLE001)


2331-2331: Unnecessary pass statement

Remove unnecessary pass

(PIE790)


2333-2333: Missing return type annotation for public function user_saves_or_removes_resource

(ANN201)


2349-2349: Avoid equality comparisons to True; use data["status"]: for truth checks

Replace with data["status"]

(E712)


2384-2384: store_resources is too complex (14 > 10)

(C901)


2384-2384: Missing return type annotation for public function store_resources

Add return type annotation: None

(ANN201)


2384-2384: PEP 484 prohibits implicit Optional

Convert to Optional[T]

(RUF013)


2384-2384: Missing type annotation for function argument recommendation_type

(ANN001)


2385-2385: PEP 484 prohibits implicit Optional

Convert to Optional[T]

(RUF013)


2385-2385: Missing type annotation for function argument resources_form

(ANN001)


2386-2386: Unused method argument: resources_updated

(ARG002)


2386-2386: PEP 484 prohibits implicit Optional

Convert to Optional[T]

(RUF013)


2386-2386: Missing type annotation for function argument resources_updated_type

(ANN001)


2386-2386: Unused method argument: resources_updated_type

(ARG002)


2404-2404: Missing return type annotation for private function get_resource_primary_key

(ANN202)


2415-2415: Logging statement uses f-string

(G004)


2422-2423: Use a single if statement instead of nested if statements

(SIM102)


2424-2424: Logging statement uses f-string

(G004)


2449-2449: Missing return type annotation for public function retrieve_resources

(ANN201)


2449-2449: Boolean default positional argument in function definition

(FBT002)


2449-2449: Missing type annotation for function argument embedding_values

(ANN001)


2455-2455: Missing return type annotation for private function resource_replace_none_value

(ANN202)


2455-2455: Missing type annotation for function argument value

(ANN001)


2456-2456: Comparison to None should be cond is None

Replace with cond is None

(E711)


2462-2462: Avoid equality comparisons to True; use embedding_values: for truth checks

Replace with embedding_values

(E712)


2513-2513: Unnecessary assignment to result before return statement

Remove unnecessary assignment

(RET504)


2515-2515: Missing return type annotation for public function retrieve_resources_by_updated_at_exist_or_counts

(ANN201)


2515-2515: Missing type annotation for function argument cids

(ANN001)


2515-2515: Boolean default positional argument in function definition

(FBT002)


2515-2515: Missing type annotation for function argument only_exist

(ANN001)


2515-2515: Missing type annotation for function argument days

(ANN001)


2526-2526: Avoid equality comparisons to True; use only_exist: for truth checks

Replace with only_exist

(E712)


2548-2548: Unnecessary assignment to count before return statement

Remove unnecessary assignment

(RET504)


2550-2550: Missing return type annotation for public function resources_wrapper_from_query

(ANN201)


2570-2570: Use resource.get("is_bookmarked_fill", False) instead of an if block

Replace with resource.get("is_bookmarked_fill", False)

(SIM401)


2571-2571: Use resource.get("saves_count", 0) instead of an if block

Replace with resource.get("saves_count", 0)

(SIM401)


2572-2572: Use resource.get("concept_cid", 0) instead of an if block

Replace with resource.get("concept_cid", 0)

(SIM401)


2592-2592: Missing return type annotation for public function update_resource_action

Add return type annotation: None

(ANN201)


2592-2592: Boolean default positional argument in function definition

(FBT002)


2592-2592: Missing type annotation for function argument action

(ANN001)


2610-2610: Missing return type annotation for public function filter_user_resources_saved_by

(ANN201)


2676-2676: Missing return type annotation for public function get_rids_from_user_saves

(ANN201)


2691-2691: Unnecessary assignment to nodes before return statement

Remove unnecessary assignment

(RET504)


2693-2693: Missing return type annotation for public function get_main_concepts_by_mid

(ANN201)


2710-2710: Unnecessary assignment to nodes before return statement

Remove unnecessary assignment

(RET504)


2712-2712: Missing return type annotation for public function get_main_concepts_by_slide_id

(ANN201)


2729-2729: Unnecessary assignment to nodes before return statement

Remove unnecessary assignment

(RET504)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build container images / Build image (coursemapper-kg/concept-map)
  • GitHub Check: Build container images / Build image (webapp)
  • GitHub Check: Build container images / Build image (webserver)
  • GitHub Check: Build container images / Build image (coursemapper-kg/recommendation)
  • GitHub Check: Build container images / Build image (coursemapper-kg/preprocess)
🔇 Additional comments (4)
webapp/src/app/pages/components/knowledge-graph/concept-map/concept-map.component.ts (3)

7-9: LGTM: required imports added correctly

ViewChild, Renderer2, CRO types, and the CRO component are appropriately imported.

Also applies to: 34-36


2304-2306: LGTM: safe optional chaining in croUpdater

This avoids errors when the child ViewChild isn’t yet available.


2317-2333: Delegate width logic to the responsive helper
Verify that the resize handler is actually bound (via @HostListener('window:resize', ['$event'])) and that a setResponsiveWidthKnowledgeGraph() helper exists before renaming setWeightGraphComponent to setWidthGraphComponent and updating its callsites.

coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py (1)

873-896: Potential signature mismatch in concept resource retrieval.

retrieve_concept_resources is defined as (tx, mid, cid) but get_concept_resources calls it with (tx, concept_id) and earlier with (session, material_id, cid). Ensure all call sites pass both mid and cid.

Would you like me to scan the repo and generate a patch to normalize this API and its usages?

Also applies to: 1023-1046

# os.environ['PYTHONHASHSEED'] = '0'
# os.execv(sys.executable, [sys.executable] + sys.argv)

from datetime import datetime

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make timestamps timezone-aware (UTC).

Use timezone-aware timestamps to avoid Cypher datetime parsing inconsistencies and DST bugs.

-from datetime import datetime
+from datetime import datetime, timezone
-                    updated_at=datetime.now().isoformat(),
+                    updated_at=datetime.now(timezone.utc).isoformat(),
-                    updated_at=datetime.now().isoformat(),
+                    updated_at=datetime.now(timezone.utc).isoformat(),
-                    updated_at=datetime.now().isoformat(),
+                    updated_at=datetime.now(timezone.utc).isoformat(),

Also applies to: 1848-1849, 1907-1907, 1984-1984

🤖 Prompt for AI Agents
In coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py
around line 16 (and also for the occurrences at lines 1848-1849, 1907, 1984),
the code currently uses naive datetime objects; change imports and usages to
produce timezone-aware UTC timestamps (e.g., import timezone from datetime and
replace datetime.now() / datetime.utcnow() usages with
datetime.now(timezone.utc) or datetime.utcnow().replace(tzinfo=timezone.utc)) so
all created timestamps are explicitly UTC-aware before being used in Cypher
queries or stored.

Comment on lines +129 to +147
def create_external_source_resource(tx, node):
"""
Create ExternalSource Node
"""
logger.info(
"Creating ExternalSource resource '%s'" % node["id"])
tx.run(
"""MERGE (c:Resource:ExternalSource {rid: $rid, uri: $uri,
publish_time: $created_at, cid: $cid, description: $description, helpful_count: $helpful_count,
not_helpful_count: $not_helpful_count, saves_count: $saves_count})""",
rid=node["uri"],
uri=node["uri"],
publish_time=node["created_at"],
description=node["description"],
cid=node["cid"],
helpful_count=0,
not_helpful_count=0,
saves_count=0
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Broken MERGE parameter names and RID; fix logger and type hints.

  • Cypher uses $created_at but you pass publish_time; this makes publish_time always null.
  • rid is set to node["uri"] instead of the resource id; likely unintended.
  • Prefer structured logging args over % formatting.
  • Add annotations.
-def create_external_source_resource(tx, node):
+def create_external_source_resource(tx, node) -> None:
@@
-    logger.info(
-        "Creating ExternalSource resource '%s'" % node["id"])
+    logger.info("Creating ExternalSource resource '%s'", node["id"])
     tx.run(
-        """MERGE (c:Resource:ExternalSource {rid: $rid, uri: $uri, 
-        publish_time: $created_at, cid: $cid, description: $description, helpful_count: $helpful_count,
+        """MERGE (c:Resource:ExternalSource {rid: $rid, uri: $uri, 
+        publish_time: $publish_time, cid: $cid, description: $description, helpful_count: $helpful_count,
         not_helpful_count: $not_helpful_count, saves_count: $saves_count})""",
-        rid=node["uri"],
+        rid=node["id"],
         uri=node["uri"],
-        publish_time=node["created_at"],
+        publish_time=node["created_at"],
         description=node["description"],
         cid=node["cid"],
         helpful_count=0,
         not_helpful_count=0,
         saves_count=0
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create_external_source_resource(tx, node):
"""
Create ExternalSource Node
"""
logger.info(
"Creating ExternalSource resource '%s'" % node["id"])
tx.run(
"""MERGE (c:Resource:ExternalSource {rid: $rid, uri: $uri,
publish_time: $created_at, cid: $cid, description: $description, helpful_count: $helpful_count,
not_helpful_count: $not_helpful_count, saves_count: $saves_count})""",
rid=node["uri"],
uri=node["uri"],
publish_time=node["created_at"],
description=node["description"],
cid=node["cid"],
helpful_count=0,
not_helpful_count=0,
saves_count=0
)
def create_external_source_resource(tx, node) -> None:
"""
Create ExternalSource Node
"""
logger.info("Creating ExternalSource resource '%s'", node["id"])
tx.run(
"""MERGE (c:Resource:ExternalSource {rid: $rid, uri: $uri,
publish_time: $publish_time, cid: $cid, description: $description, helpful_count: $helpful_count,
not_helpful_count: $not_helpful_count, saves_count: $saves_count})""",
rid=node["id"],
uri=node["uri"],
publish_time=node["created_at"],
description=node["description"],
cid=node["cid"],
helpful_count=0,
not_helpful_count=0,
saves_count=0
)
🧰 Tools
🪛 Ruff (0.12.2)

129-129: Missing return type annotation for public function create_external_source_resource

Add return type annotation: None

(ANN201)


129-129: Missing type annotation for function argument tx

(ANN001)


129-129: Missing type annotation for function argument node

(ANN001)


134-134: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


134-134: Logging statement uses %

(G002)

Comment on lines +1795 to +1815
def get_or_create_user_v2(self, user):
"""
"""
tx = self.driver.session()
user_node = tx.run(
"MATCH (u:User) WHERE u.uid = $uid RETURN u.uid as uid",
uid=user["user_id"]
).single()

if user_node is None:
user_node = tx.run(
"""MERGE (u:User {name: $name, uid: $uid, type: $type, email: $userEmail, embedding:$embedding}) RETURN u.uid""",
name=user["name"],
uid=user["user_id"],
type="user",
userEmail=user["user_email"],
embedding=""
).single()

return user_node

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Resource leak and naming: open session is never closed.

Use a context manager; return normalized shape (uid string).

-def get_or_create_user_v2(self, user):
+def get_or_create_user_v2(self, user) -> str:
@@
-        tx = self.driver.session()
-        user_node = tx.run(
+        with self.driver.session() as session:
+            record = session.run(
                 "MATCH (u:User) WHERE u.uid = $uid RETURN u.uid as uid",
                 uid=user["user_id"]
-        ).single()
-
-        if user_node is None:
-            user_node = tx.run(
+            ).single()
+
+            if record is None:
+                record = session.run(
                     """MERGE (u:User {name: $name, uid: $uid, type: $type, email: $userEmail, embedding:$embedding}) RETURN u.uid as uid""",
                     name=user["name"],
                     uid=user["user_id"],
                     type="user",
                     userEmail=user["user_email"],
                     embedding=""
-                ).single()
-            
-        return user_node
+                ).single()
+            return record["uid"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_or_create_user_v2(self, user):
"""
"""
tx = self.driver.session()
user_node = tx.run(
"MATCH (u:User) WHERE u.uid = $uid RETURN u.uid as uid",
uid=user["user_id"]
).single()
if user_node is None:
user_node = tx.run(
"""MERGE (u:User {name: $name, uid: $uid, type: $type, email: $userEmail, embedding:$embedding}) RETURN u.uid""",
name=user["name"],
uid=user["user_id"],
type="user",
userEmail=user["user_email"],
embedding=""
).single()
return user_node
def get_or_create_user_v2(self, user) -> str:
"""
"""
with self.driver.session() as session:
record = session.run(
"MATCH (u:User) WHERE u.uid = $uid RETURN u.uid as uid",
uid=user["user_id"]
).single()
if record is None:
record = session.run(
"""MERGE (u:User {name: $name, uid: $uid, type: $type, email: $userEmail, embedding:$embedding}) RETURN u.uid as uid""",
name=user["name"],
uid=user["user_id"],
type="user",
userEmail=user["user_email"],
embedding=""
).single()
return record["uid"]
🧰 Tools
🪛 Ruff (0.12.2)

1795-1795: Missing return type annotation for public function get_or_create_user_v2

(ANN201)


1795-1795: Missing type annotation for function argument user

(ANN001)

🤖 Prompt for AI Agents
In coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py
around lines 1795-1815 the Neo4j session is opened but never closed and the
function returns a raw record instead of a normalized uid string; fix by using a
context manager (with self.driver.session() as session:) and call
session.run(...) for the MATCH and MERGE queries, retrieve the uid from the
resulting record (e.g. record["uid"] or record.get("uid")) and return that uid
string (not the Record object), and ensure the MERGE/parameters match the same
uid field so the function always returns a consistent uid string.

Comment on lines +1816 to +1913
def create_or_update_video_resource(self, tx, node: dict,
recommendation_type='',
update_embedding_values=False,
update_detail_found=False
):
'''
Creating Resource YouTube
r.similarity_score = $similarity_score,
'''
# logger.info(" Creating Resource YouTube")
try:
"""
if update_detail_found == True:
tx.run(
'''
MATCH (r:Resource: Video)
WHERE r.rid = $rid
SET r.title = $title, r.description = $description, r.description_full = $description_full,
r.text = $text, r.duration = $duration, r.views = $views, like_count = $like_count,
r.channel_title = $channel_title, r.updated_at = $updated_at,
r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding,
r.document_embedding = $document_embedding
''',
rid=node["rid"],
title=node["title"],
description=node["description"],
description_full=node["description_full"],
text=node["text"],
duration=node["duration"],
views=node["views"],
like_count=node["like_count"],
channel_title=node["channel_title"],
updated_at=datetime.now().isoformat(),
keyphrases=[],
keyphrase_embedding="",
document_embedding=""
)
"""

if update_embedding_values == True: # node.get("keyphrases") != None or node.get("document_embedding") != None or node.get("keyphrase_embedding") != None:
tx.run(
'''
MATCH (r:Resource: Video)
WHERE r.rid = $rid
SET r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding, r.document_embedding = $document_embedding,
r.keyphrases_infos = $keyphrases_infos

''',
rid=node["rid"],
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
document_embedding=node["document_embedding"] if "document_embedding" in node else [],
keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
)
else:
tx.run(
'''
MERGE (r:Resource:Video {rid: $rid})
ON CREATE SET
r.uri = $uri, r.title = $title, r.description = $description, r.description_full = $description_full, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.thumbnail = $thumbnail, r.duration = $duration, r.views = $views,
r.publish_time = $pub_time, r.channel_title = $channel_title, r.like_count = $like_count,
r.helpful_count = $helpful_count, r.not_helpful_count = $not_helpful_count, r.saves_count = $saves_count,
r.updated_at = $updated_at
ON MATCH SET
r.title = $title, r.description = $description, r.description_full = $description_full, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.thumbnail = $thumbnail, r.duration = $duration, r.views = $views,
r.publish_time = $pub_time, r.channel_title = $channel_title, r.like_count = $like_count,
r.updated_at = $updated_at
''',
rid=node["id"],
uri="https://www.youtube.com/embed/%s?autoplay=1" % node["id"],
title=node["title"],
description=node["description"],
description_full=node["description_full"],
thumbnail="https://i.ytimg.com/vi/%s/hqdefault.jpg" % node["id"],
text=node["text"],
duration=node["duration"],
views=node["views"],
pub_time=node["publishTime"],
# similarity_score=node[recommendation_type] if recommendation_type in node.index else 0,
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
document_embedding=node["document_embedding"] if "document_embedding" in node else [],
helpful_count=node["helpful_count"] if "helpful_count" in node else 0,
not_helpful_count=node["not_helpful_count"] if "not_helpful_count" in node else 0,
saves_count=node["saves_count"] if "saves_count" in node else 0,
like_count=node["like_count"],
channel_title=node["channel_title"],
updated_at=datetime.now().isoformat(),
keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
)
except Exception as e:
print(e)
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden video upsert: defaults, boolean checks, formatting, and exceptions.

  • Use .get() for optional fields to avoid KeyError.
  • Prefer simple truth checks over == True.
  • Use f-strings for string building (not for logging).
  • Replace bare except with logging the exception.
-            if update_embedding_values == True: # node.get("keyphrases") != None or node.get("document_embedding") != None or node.get("keyphrase_embedding") != None:
+            if update_embedding_values:
@@
-                    keyphrases=node["keyphrases"] if "keyphrases" in node else [],
-                    keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
-                    document_embedding=node["document_embedding"] if "document_embedding" in node else [],
-                    keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
+                    keyphrases=node.get("keyphrases", []),
+                    keyphrase_embedding=node.get("keyphrase_embedding", []),
+                    document_embedding=node.get("document_embedding", []),
+                    keyphrases_infos=node.get("keyphrases_infos", "")
                 )
             else:
                 tx.run(
@@
-                    rid=node["id"],
-                    uri="https://www.youtube.com/embed/%s?autoplay=1" % node["id"],
+                    rid=node["id"],
+                    uri=f"https://www.youtube.com/embed/{node['id']}?autoplay=1",
@@
-                    thumbnail="https://i.ytimg.com/vi/%s/hqdefault.jpg" % node["id"],
+                    thumbnail=f"https://i.ytimg.com/vi/{node['id']}/hqdefault.jpg",
@@
-                    keyphrases=node["keyphrases"] if "keyphrases" in node else [],
-                    keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
-                    document_embedding=node["document_embedding"] if "document_embedding" in node else [],
-                    helpful_count=node["helpful_count"] if "helpful_count" in node else 0,
-                    not_helpful_count=node["not_helpful_count"] if "not_helpful_count" in node else 0,
-                    saves_count=node["saves_count"] if "saves_count" in node else 0,
+                    keyphrases=node.get("keyphrases", []),
+                    keyphrase_embedding=node.get("keyphrase_embedding", []),
+                    document_embedding=node.get("document_embedding", []),
+                    helpful_count=node.get("helpful_count", 0),
+                    not_helpful_count=node.get("not_helpful_count", 0),
+                    saves_count=node.get("saves_count", 0),
@@
-                    updated_at=datetime.now().isoformat(),
-                    keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
+                    updated_at=datetime.now(timezone.utc).isoformat(),
+                    keyphrases_infos=node.get("keyphrases_infos", "")
                 )
-        except Exception as e:
-            print(e)
-            pass
+        except Exception as e:
+            logger.exception("create_or_update_video_resource failed: %s", e)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create_or_update_video_resource(self, tx, node: dict,
recommendation_type='',
update_embedding_values=False,
update_detail_found=False
):
'''
Creating Resource YouTube
r.similarity_score = $similarity_score,
'''
# logger.info(" Creating Resource YouTube")
try:
"""
if update_detail_found == True:
tx.run(
'''
MATCH (r:Resource: Video)
WHERE r.rid = $rid
SET r.title = $title, r.description = $description, r.description_full = $description_full,
r.text = $text, r.duration = $duration, r.views = $views, like_count = $like_count,
r.channel_title = $channel_title, r.updated_at = $updated_at,
r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding,
r.document_embedding = $document_embedding
''',
rid=node["rid"],
title=node["title"],
description=node["description"],
description_full=node["description_full"],
text=node["text"],
duration=node["duration"],
views=node["views"],
like_count=node["like_count"],
channel_title=node["channel_title"],
updated_at=datetime.now().isoformat(),
keyphrases=[],
keyphrase_embedding="",
document_embedding=""
)
"""
if update_embedding_values == True: # node.get("keyphrases") != None or node.get("document_embedding") != None or node.get("keyphrase_embedding") != None:
tx.run(
'''
MATCH (r:Resource: Video)
WHERE r.rid = $rid
SET r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding, r.document_embedding = $document_embedding,
r.keyphrases_infos = $keyphrases_infos
''',
rid=node["rid"],
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
document_embedding=node["document_embedding"] if "document_embedding" in node else [],
keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
)
else:
tx.run(
'''
MERGE (r:Resource:Video {rid: $rid})
ON CREATE SET
r.uri = $uri, r.title = $title, r.description = $description, r.description_full = $description_full, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.thumbnail = $thumbnail, r.duration = $duration, r.views = $views,
r.publish_time = $pub_time, r.channel_title = $channel_title, r.like_count = $like_count,
r.helpful_count = $helpful_count, r.not_helpful_count = $not_helpful_count, r.saves_count = $saves_count,
r.updated_at = $updated_at
ON MATCH SET
r.title = $title, r.description = $description, r.description_full = $description_full, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.thumbnail = $thumbnail, r.duration = $duration, r.views = $views,
r.publish_time = $pub_time, r.channel_title = $channel_title, r.like_count = $like_count,
r.updated_at = $updated_at
''',
rid=node["id"],
uri="https://www.youtube.com/embed/%s?autoplay=1" % node["id"],
title=node["title"],
description=node["description"],
description_full=node["description_full"],
thumbnail="https://i.ytimg.com/vi/%s/hqdefault.jpg" % node["id"],
text=node["text"],
duration=node["duration"],
views=node["views"],
pub_time=node["publishTime"],
# similarity_score=node[recommendation_type] if recommendation_type in node.index else 0,
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
document_embedding=node["document_embedding"] if "document_embedding" in node else [],
helpful_count=node["helpful_count"] if "helpful_count" in node else 0,
not_helpful_count=node["not_helpful_count"] if "not_helpful_count" in node else 0,
saves_count=node["saves_count"] if "saves_count" in node else 0,
like_count=node["like_count"],
channel_title=node["channel_title"],
updated_at=datetime.now().isoformat(),
keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
)
except Exception as e:
print(e)
pass
def create_or_update_video_resource(self, tx, node: dict,
recommendation_type='',
update_embedding_values=False,
update_detail_found=False
):
'''
Creating Resource YouTube
r.similarity_score = $similarity_score,
'''
# logger.info(" Creating Resource YouTube")
try:
"""
if update_detail_found == True:
tx.run(
'''
MATCH (r:Resource: Video)
WHERE r.rid = $rid
SET r.title = $title, r.description = $description, r.description_full = $description_full,
r.text = $text, r.duration = $duration, r.views = $views, like_count = $like_count,
r.channel_title = $channel_title, r.updated_at = $updated_at,
r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding,
r.document_embedding = $document_embedding
''',
rid=node["rid"],
title=node["title"],
description=node["description"],
description_full=node["description_full"],
text=node["text"],
duration=node["duration"],
views=node["views"],
like_count=node["like_count"],
channel_title=node["channel_title"],
updated_at=datetime.now(timezone.utc).isoformat(),
keyphrases=[],
keyphrase_embedding="",
document_embedding=""
)
"""
if update_embedding_values:
tx.run(
'''
MATCH (r:Resource: Video)
WHERE r.rid = $rid
SET r.keyphrases = $keyphrases,
r.keyphrase_embedding = $keyphrase_embedding,
r.document_embedding = $document_embedding,
r.keyphrases_infos = $keyphrases_infos
''',
rid=node["rid"],
keyphrases=node.get("keyphrases", []),
keyphrase_embedding=node.get("keyphrase_embedding", []),
document_embedding=node.get("document_embedding", []),
keyphrases_infos=node.get("keyphrases_infos", "")
)
else:
tx.run(
'''
MERGE (r:Resource:Video {rid: $rid})
ON CREATE SET
r.uri = $uri, r.title = $title, r.description = $description,
r.description_full = $description_full, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding,
r.keyphrase_embedding = $keyphrase_embedding,
r.thumbnail = $thumbnail, r.duration = $duration,
r.views = $views, r.publish_time = $pub_time,
r.channel_title = $channel_title, r.like_count = $like_count,
r.helpful_count = $helpful_count, r.not_helpful_count = $not_helpful_count,
r.saves_count = $saves_count, r.updated_at = $updated_at
ON MATCH SET
r.title = $title, r.description = $description,
r.description_full = $description_full, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding,
r.keyphrase_embedding = $keyphrase_embedding,
r.thumbnail = $thumbnail, r.duration = $duration,
r.views = $views, r.publish_time = $pub_time,
r.channel_title = $channel_title, r.like_count = $like_count,
r.updated_at = $updated_at
''',
rid=node["id"],
uri=f"https://www.youtube.com/embed/{node['id']}?autoplay=1",
title=node["title"],
description=node["description"],
description_full=node["description_full"],
thumbnail=f"https://i.ytimg.com/vi/{node['id']}/hqdefault.jpg",
text=node["text"],
duration=node["duration"],
views=node["views"],
pub_time=node["publishTime"],
keyphrases=node.get("keyphrases", []),
keyphrase_embedding=node.get("keyphrase_embedding", []),
document_embedding=node.get("document_embedding", []),
helpful_count=node.get("helpful_count", 0),
not_helpful_count=node.get("not_helpful_count", 0),
saves_count=node.get("saves_count", 0),
like_count=node["like_count"],
channel_title=node["channel_title"],
updated_at=datetime.now(timezone.utc).isoformat(),
keyphrases_infos=node.get("keyphrases_infos", "")
)
except Exception as e:
logger.exception("create_or_update_video_resource failed: %s", e)
🧰 Tools
🪛 Ruff (0.12.2)

1816-1816: Missing return type annotation for public function create_or_update_video_resource

Add return type annotation: None

(ANN201)


1816-1816: Missing type annotation for function argument tx

(ANN001)


1817-1817: Missing type annotation for function argument recommendation_type

(ANN001)


1817-1817: Unused method argument: recommendation_type

(ARG002)


1818-1818: Boolean default positional argument in function definition

(FBT002)


1818-1818: Missing type annotation for function argument update_embedding_values

(ANN001)


1819-1819: Boolean default positional argument in function definition

(FBT002)


1819-1819: Missing type annotation for function argument update_detail_found

(ANN001)


1819-1819: Unused method argument: update_detail_found

(ARG002)


1855-1855: Avoid equality comparisons to True; use update_embedding_values: for truth checks

Replace with update_embedding_values

(E712)


1865-1865: Use node.get("keyphrases", []) instead of an if block

Replace with node.get("keyphrases", [])

(SIM401)


1866-1866: Use node.get("keyphrase_embedding", []) instead of an if block

Replace with node.get("keyphrase_embedding", [])

(SIM401)


1867-1867: Use node.get("document_embedding", []) instead of an if block

Replace with node.get("document_embedding", [])

(SIM401)


1868-1868: Use node.get("keyphrases_infos", "") instead of an if block

Replace with node.get("keyphrases_infos", "")

(SIM401)


1889-1889: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1893-1893: Use format specifiers instead of percent format

Replace with format specifiers

(UP031)


1899-1899: Use node.get("keyphrases", []) instead of an if block

Replace with node.get("keyphrases", [])

(SIM401)


1900-1900: Use node.get("keyphrase_embedding", []) instead of an if block

Replace with node.get("keyphrase_embedding", [])

(SIM401)


1901-1901: Use node.get("document_embedding", []) instead of an if block

Replace with node.get("document_embedding", [])

(SIM401)


1902-1902: Use node.get("helpful_count", 0) instead of an if block

Replace with node.get("helpful_count", 0)

(SIM401)


1903-1903: Use node.get("not_helpful_count", 0) instead of an if block

Replace with node.get("not_helpful_count", 0)

(SIM401)


1904-1904: Use node.get("saves_count", 0) instead of an if block

Replace with node.get("saves_count", 0)

(SIM401)


1907-1907: datetime.datetime.now() called without a tz argument

(DTZ005)


1908-1908: Use node.get("keyphrases_infos", "") instead of an if block

Replace with node.get("keyphrases_infos", "")

(SIM401)


1910-1910: Do not catch blind exception: Exception

(BLE001)


1912-1912: Unnecessary pass statement

Remove unnecessary pass

(PIE790)

🤖 Prompt for AI Agents
In coursemapper-kg/recommendation/app/services/course_materials/db/neo4_db.py
around lines 1816 to 1913, the video upsert is fragile: replace direct indexing
like node["..."] with node.get("...") and provide sensible defaults (e.g. empty
list or 0) to avoid KeyError; change the conditional check from
update_embedding_values == True to a simple truthy check (if
update_embedding_values:); build uri and thumbnail using f-strings (e.g.
f"https://www.youtube.com/embed/{node.get('id')}?autoplay=1") instead of
%-formatting; and replace the current bare/print exception handling with a
proper logger call that logs the exception and stack (e.g. logger.exception or
logger.error with exc_info=True) rather than swallowing errors.

Comment on lines +1914 to +1990
def create_or_update_wikipedia_resource(self, tx, node, recommendation_type='',
update_embedding_values=False,
update_detail_found=False
):
'''
Creating Resource Wikipedia
'''
# logger.info("Creating Resource Wikipedia")
try:
"""
if update_detail_found == True:
tx.run(
'''
MATCH (r:Resource: Article)
WHERE r.rid = $rid
SET r.title = $title, r.abstract = $abstract, r.text = $text, updated_at = $updated_at,
r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding,
r.document_embedding = $document_embedding
''',
rid=node["rid"],
title=node["title"],
abstract=node["abstract"],
text=node["text"],
updated_at=datetime.now().isoformat(),
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=str(node["keyphrase_embedding"] if "keyphrase_embedding" in node else ""),
document_embedding=str(node["document_embedding"] if "document_embedding" in node else ""),
)
"""

if update_embedding_values == True: # node.get("keyphrases") != None or node.get("document_embedding") != None or node.get("keyphrase_embedding") != None:
tx.run(
'''
MATCH (r:Resource: Article)
WHERE r.rid = $rid
SET r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding, r.document_embedding = $document_embedding,
r.keyphrases_infos = $keyphrases_infos
''',
rid=node["rid"],
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
document_embedding=node["document_embedding"] if "document_embedding" in node else [],
keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
)
else:
tx.run(
'''
MERGE (r:Resource:Article {rid: $rid})
ON CREATE SET
r.uri = $uri, r.title = $title, r.abstract = $abstract, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.helpful_count = $helpful_count, r.not_helpful_count = $not_helpful_count, r.saves_count = $saves_count,
r.updated_at = $updated_at
ON MATCH SET
r.uri = $uri, r.title = $title, r.abstract = $abstract, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.updated_at = $updated_at
''',
rid=node["id"],
uri=node["id"],
title=node["title"],
abstract=node["abstract"],
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
text=node["text"],
# similarity_score=node[recommendation_type] if recommendation_type in node.index else 0,
keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
document_embedding=node["document_embedding"] if "document_embedding" in node else [],
helpful_count=node["helpful_count"] if "helpful_count" in node else 0,
not_helpful_count=node["not_helpful_count"] if "not_helpful_count" in node else 0,
saves_count=node["saves_count"] if "saves_count" in node else 0,
updated_at=datetime.now().isoformat(),
keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
)
except Exception as e:
print(e)
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden article upsert similarly; avoid bare exceptions and make timestamps UTC.

-            if update_embedding_values == True: # node.get("keyphrases") != None or node.get("document_embedding") != None or node.get("keyphrase_embedding") != None:
+            if update_embedding_values:
@@
-                    keyphrases=node["keyphrases"] if "keyphrases" in node else [],
-                    keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
-                    document_embedding=node["document_embedding"] if "document_embedding" in node else [],
-                    keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
+                    keyphrases=node.get("keyphrases", []),
+                    keyphrase_embedding=node.get("keyphrase_embedding", []),
+                    document_embedding=node.get("document_embedding", []),
+                    keyphrases_infos=node.get("keyphrases_infos", "")
                 )
             else:
                 tx.run(
@@
-                    keyphrases=node["keyphrases"] if "keyphrases" in node else [],
+                    keyphrases=node.get("keyphrases", []),
@@
-                    keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
-                    document_embedding=node["document_embedding"] if "document_embedding" in node else [],
-                    helpful_count=node["helpful_count"] if "helpful_count" in node else 0,
-                    not_helpful_count=node["not_helpful_count"] if "not_helpful_count" in node else 0,
-                    saves_count=node["saves_count"] if "saves_count" in node else 0,
-                    updated_at=datetime.now().isoformat(),
-                    keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
+                    keyphrase_embedding=node.get("keyphrase_embedding", []),
+                    document_embedding=node.get("document_embedding", []),
+                    helpful_count=node.get("helpful_count", 0),
+                    not_helpful_count=node.get("not_helpful_count", 0),
+                    saves_count=node.get("saves_count", 0),
+                    updated_at=datetime.now(timezone.utc).isoformat(),
+                    keyphrases_infos=node.get("keyphrases_infos", "")
                 )
-        except Exception as e:
-            print(e)
-            pass
+        except Exception as e:
+            logger.exception("create_or_update_wikipedia_resource failed: %s", e)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create_or_update_wikipedia_resource(self, tx, node, recommendation_type='',
update_embedding_values=False,
update_detail_found=False
):
'''
Creating Resource Wikipedia
'''
# logger.info("Creating Resource Wikipedia")
try:
"""
if update_detail_found == True:
tx.run(
'''
MATCH (r:Resource: Article)
WHERE r.rid = $rid
SET r.title = $title, r.abstract = $abstract, r.text = $text, updated_at = $updated_at,
r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding,
r.document_embedding = $document_embedding
''',
rid=node["rid"],
title=node["title"],
abstract=node["abstract"],
text=node["text"],
updated_at=datetime.now().isoformat(),
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=str(node["keyphrase_embedding"] if "keyphrase_embedding" in node else ""),
document_embedding=str(node["document_embedding"] if "document_embedding" in node else ""),
)
"""
if update_embedding_values == True: # node.get("keyphrases") != None or node.get("document_embedding") != None or node.get("keyphrase_embedding") != None:
tx.run(
'''
MATCH (r:Resource: Article)
WHERE r.rid = $rid
SET r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding, r.document_embedding = $document_embedding,
r.keyphrases_infos = $keyphrases_infos
''',
rid=node["rid"],
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
document_embedding=node["document_embedding"] if "document_embedding" in node else [],
keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
)
else:
tx.run(
'''
MERGE (r:Resource:Article {rid: $rid})
ON CREATE SET
r.uri = $uri, r.title = $title, r.abstract = $abstract, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.helpful_count = $helpful_count, r.not_helpful_count = $not_helpful_count, r.saves_count = $saves_count,
r.updated_at = $updated_at
ON MATCH SET
r.uri = $uri, r.title = $title, r.abstract = $abstract, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.updated_at = $updated_at
''',
rid=node["id"],
uri=node["id"],
title=node["title"],
abstract=node["abstract"],
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
text=node["text"],
# similarity_score=node[recommendation_type] if recommendation_type in node.index else 0,
keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
document_embedding=node["document_embedding"] if "document_embedding" in node else [],
helpful_count=node["helpful_count"] if "helpful_count" in node else 0,
not_helpful_count=node["not_helpful_count"] if "not_helpful_count" in node else 0,
saves_count=node["saves_count"] if "saves_count" in node else 0,
updated_at=datetime.now().isoformat(),
keyphrases_infos=node["keyphrases_infos"] if "keyphrases_infos" in node else ""
)
except Exception as e:
print(e)
pass
def create_or_update_wikipedia_resource(self, tx, node, recommendation_type='',
update_embedding_values=False,
update_detail_found=False
):
'''
Creating Resource Wikipedia
'''
# logger.info("Creating Resource Wikipedia")
try:
"""
if update_detail_found == True:
tx.run(
'''
MATCH (r:Resource: Article)
WHERE r.rid = $rid
SET r.title = $title, r.abstract = $abstract, r.text = $text, updated_at = $updated_at,
r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding,
r.document_embedding = $document_embedding
''',
rid=node["rid"],
title=node["title"],
abstract=node["abstract"],
text=node["text"],
updated_at=datetime.now().isoformat(),
keyphrases=node["keyphrases"] if "keyphrases" in node else [],
keyphrase_embedding=str(node["keyphrase_embedding"] if "keyphrase_embedding" in node else ""),
document_embedding=str(node["document_embedding"] if "document_embedding" in node else ""),
)
"""
if update_embedding_values:
tx.run(
'''
MATCH (r:Resource: Article)
WHERE r.rid = $rid
SET r.keyphrases = $keyphrases, r.keyphrase_embedding = $keyphrase_embedding, r.document_embedding = $document_embedding,
r.keyphrases_infos = $keyphrases_infos
''',
rid=node["rid"],
- keyphrases=node["keyphrases"] if "keyphrases" in node else [],
- keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
- document_embedding=node["document_embedding"] if "document_embedding" in node else [],
keyphrases=node.get("keyphrases", []),
keyphrase_embedding=node.get("keyphrase_embedding", []),
document_embedding=node.get("document_embedding", []),
keyphrases_infos=node.get("keyphrases_infos", "")
)
else:
tx.run(
'''
MERGE (r:Resource:Article {rid: $rid})
ON CREATE SET
r.uri = $uri, r.title = $title, r.abstract = $abstract, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.helpful_count = $helpful_count, r.not_helpful_count = $not_helpful_count, r.saves_count = $saves_count,
r.updated_at = $updated_at
ON MATCH SET
r.uri = $uri, r.title = $title, r.abstract = $abstract, r.text = $text,
r.keyphrases = $keyphrases, r.document_embedding = $document_embedding, r.keyphrase_embedding = $keyphrase_embedding,
r.updated_at = $updated_at
''',
rid=node["id"],
uri=node["id"],
title=node["title"],
abstract=node["abstract"],
keyphrases=node.get("keyphrases", []),
text=node["text"],
# similarity_score=node[recommendation_type] if recommendation_type in node.index else 0,
- keyphrase_embedding=node["keyphrase_embedding"] if "keyphrase_embedding" in node else [],
- document_embedding=node["document_embedding"] if "document_embedding" in node else [],
- helpful_count=node["helpful_count"] if "helpful_count" in node else 0,
- not_helpful_count=node["not_helpful_count"] if "not_helpful_count" in node else 0,
- saves_count=node["saves_count"] if "saves_count" in node else 0,
- updated_at=datetime.now().isoformat(),
keyphrase_embedding=node.get("keyphrase_embedding", []),
document_embedding=node.get("document_embedding", []),
helpful_count=node.get("helpful_count", 0),
not_helpful_count=node.get("not_helpful_count", 0),
saves_count=node.get("saves_count", 0),
updated_at=datetime.now(timezone.utc).isoformat(),
keyphrases_infos=node.get("keyphrases_infos", "")
)
- except Exception as e:
- print(e)
except Exception as e:
logger.exception("create_or_update_wikipedia_resource failed: %s", e)
🧰 Tools
🪛 Ruff (0.12.2)

1914-1914: Missing return type annotation for public function create_or_update_wikipedia_resource

Add return type annotation: None

(ANN201)


1914-1914: Missing type annotation for function argument tx

(ANN001)


1914-1914: Missing type annotation for function argument node

(ANN001)


1914-1914: Missing type annotation for function argument recommendation_type

(ANN001)


1914-1914: Unused method argument: recommendation_type

(ARG002)


1915-1915: Boolean default positional argument in function definition

(FBT002)


1915-1915: Missing type annotation for function argument update_embedding_values

(ANN001)


1916-1916: Boolean default positional argument in function definition

(FBT002)


1916-1916: Missing type annotation for function argument update_detail_found

(ANN001)


1916-1916: Unused method argument: update_detail_found

(ARG002)


1944-1944: Avoid equality comparisons to True; use update_embedding_values: for truth checks

Replace with update_embedding_values

(E712)


1953-1953: Use node.get("keyphrases", []) instead of an if block

Replace with node.get("keyphrases", [])

(SIM401)


1954-1954: Use node.get("keyphrase_embedding", []) instead of an if block

Replace with node.get("keyphrase_embedding", [])

(SIM401)


1955-1955: Use node.get("document_embedding", []) instead of an if block

Replace with node.get("document_embedding", [])

(SIM401)


1956-1956: Use node.get("keyphrases_infos", "") instead of an if block

Replace with node.get("keyphrases_infos", "")

(SIM401)


1976-1976: Use node.get("keyphrases", []) instead of an if block

Replace with node.get("keyphrases", [])

(SIM401)


1979-1979: Use node.get("keyphrase_embedding", []) instead of an if block

Replace with node.get("keyphrase_embedding", [])

(SIM401)


1980-1980: Use node.get("document_embedding", []) instead of an if block

Replace with node.get("document_embedding", [])

(SIM401)


1981-1981: Use node.get("helpful_count", 0) instead of an if block

Replace with node.get("helpful_count", 0)

(SIM401)


1982-1982: Use node.get("not_helpful_count", 0) instead of an if block

Replace with node.get("not_helpful_count", 0)

(SIM401)


1983-1983: Use node.get("saves_count", 0) instead of an if block

Replace with node.get("saves_count", 0)

(SIM401)


1984-1984: datetime.datetime.now() called without a tz argument

(DTZ005)


1985-1985: Use node.get("keyphrases_infos", "") instead of an if block

Replace with node.get("keyphrases_infos", "")

(SIM401)


1987-1987: Do not catch blind exception: Exception

(BLE001)


1989-1989: Unnecessary pass statement

Remove unnecessary pass

(PIE790)

Comment on lines +210 to +225
value="video"
[(ngModel)]="filteringParamsSavedTab.content_type"
inputId="video"
(onClick)="onContentTypeChange($event)">
</p-radioButton>
<label for="video" class="ml-1">Video</label>
</div>
<div class="flex flex-row items-center">
<p-radioButton
name="article"
value="article"
[(ngModel)]="filteringParamsSavedTab.content_type"
inputId="article"
(onClick)="onContentTypeChange($event)">
</p-radioButton>
<label for="article" class="ml-1">Article</label>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Confirm PrimeNG radio events.

Ensure p-radioButton actually emits onClick; if not, prefer (ngModelChange) to react to selection.

-                  (onClick)="onContentTypeChange($event)">
+                  (ngModelChange)="onContentTypeChange($event)">
...
-                  (onClick)="onContentTypeChange($event)">
+                  (ngModelChange)="onContentTypeChange($event)">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name="video"
value="video"
[(ngModel)]="filteringParamsSavedTab.content_type"
inputId="video"
(onClick)="onContentTypeChange($event)">
</p-radioButton>
<label for="video" class="ml-1">Video</label>
</div>
<div class="flex flex-row items-center">
<p-radioButton
name="article"
value="article"
[(ngModel)]="filteringParamsSavedTab.content_type"
inputId="article"
(onClick)="onContentTypeChange($event)">
</p-radioButton>
<div class="flex flex-row items-center">
<p-radioButton
name="video"
value="video"
[(ngModel)]="filteringParamsSavedTab.content_type"
inputId="video"
(ngModelChange)="onContentTypeChange($event)">
</p-radioButton>
<label for="video" class="ml-1">Video</label>
</div>
<div class="flex flex-row items-center">
<p-radioButton
name="article"
value="article"
[(ngModel)]="filteringParamsSavedTab.content_type"
inputId="article"
(ngModelChange)="onContentTypeChange($event)">
</p-radioButton>
🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/result-view/result-view.component.html
around lines 209 to 224, the template is binding (onClick) to PrimeNG
p-radioButton which does not reliably emit that event; replace the click binding
with (ngModelChange) so selection updates trigger the handler (e.g.
(ngModelChange)="onContentTypeChange($event)") and ensure the component's
onContentTypeChange accepts the new value (string) rather than a MouseEvent;
update both radio buttons accordingly to use the same ngModel and ngModelChange
handler.

Comment on lines +347 to +348
Total Articles: {{resourcesPagination?.nodes?.articles?.total_items}}
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Wrong binding: Articles total gated by Videos total.

The label visibility for Articles is controlled by the Videos total.

-      <div class="my-auto mx-1 text-color" *ngIf="resourcesPagination?.nodes?.videos?.total_items">
-        Total Articles: {{resourcesPagination?.nodes?.articles?.total_items}}
+      <div class="my-auto mx-1 text-color" *ngIf="resourcesPagination?.nodes?.articles?.total_items">
+        Total Articles: {{ resourcesPagination?.nodes?.articles?.total_items }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="my-auto mx-1 text-color" *ngIf="resourcesPagination?.nodes?.videos?.total_items">
Total Articles: {{resourcesPagination?.nodes?.articles?.total_items}}
<div class="my-auto mx-1 text-color" *ngIf="resourcesPagination?.nodes?.articles?.total_items">
Total Articles: {{ resourcesPagination?.nodes?.articles?.total_items }}
🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/result-view/result-view.component.html
around lines 346 to 347, the "Total Articles" label is conditionally shown using
resourcesPagination?.nodes?.videos?.total_items which hides the articles count
when videos are zero; change the *ngIf to check
resourcesPagination?.nodes?.articles?.total_items instead (use the articles
total_items null-safe path) so the Articles label visibility is driven by the
articles count.

<ng-template [ngIf]="!showVideo" [ngIfElse]="watchvideo">
<div class="" *ngFor="let videoElement of videoElements">
<app-card-video
<div class="grid p-8 gap-8 sm:grid-cols-1 ">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

A11y: add list semantics and an accessible name for the icon-only button; mark strings for i18n

Improves screen-reader UX and localization readiness.

-<div class="grid p-8 gap-8 sm:grid-cols-1 ">
+<div class="grid p-8 gap-8 sm:grid-cols-1" role="list">
@@
-        <app-card-video
+        <app-card-video
+          role="listitem"
@@
-      <button id="backToList"
+      <button id="backToList"
               pButton
               type="button"
               (click)="goBack()"
               icon="pi pi-chevron-left"
-              pTooltip="Go back to list"
+              pTooltip="Go back to list"
+              i18n-pTooltip="@@backToListTooltip"
+              aria-label="Go back to list"
+              i18n-aria-label="@@backToListAriaLabel"
               class="bg-[#0277BD] p-button-rounded"
       ></button>

Also applies to: 4-13, 22-29

🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/videos/card-video-list/card-video-list.component.html
around line 1 (also applies to lines 4-13 and 22-29): the container and its
child items lack list semantics, the icon-only button has no accessible name,
and visible strings are not marked for i18n; fix by giving the container proper
list semantics (replace the generic div with a semantic <ul> or add
role="list"), ensure each item is a <li> or has role="listitem", add an
accessible name to the icon-only button (aria-label or aria-labelledby) with a
translatable string, and mark all user-facing text with the framework i18n
attributes (e.g., i18n or translate) so strings are ready for localization.

<app-card-video
<div class="grid p-8 gap-8 sm:grid-cols-1 ">
<ng-template [ngIf]="!showVideo" [ngIfElse]="watchvideo">
<div class="" *ngFor="let videoElement of videoElements">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Prefer structural *ngIf with else over [ngIf]/[ngIfElse] templates

Reduces extra DOM and improves readability.

-    <ng-template [ngIf]="!showVideo" [ngIfElse]="watchvideo">
+    <ng-container *ngIf="!showVideo; else watchvideo">
 ...
-    </ng-template>
+    </ng-container>

Also applies to: 15-15

🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/videos/card-video-list/card-video-list.component.html
around lines 2 and 15, the template uses the property bindings [ngIf] and
[ngIfElse]; replace those with the structural directive form (*ngIf="!showVideo;
else watchvideo") to reduce extra DOM and improve readability, and apply the
same replacement at line 15 where the pattern repeats; ensure the named
ng-template for "watchvideo" remains defined and referenced unchanged.

Comment on lines +17 to +22
<app-watch-video
[video]="video"
[notUnderstoodConcepts]="notUnderstoodConcepts"
(onWatchVideo)="readVideo($event)"
[videoElement]="videoElement"
[currentMaterial]="currentMaterial"
></app-card-video>
</div>
</ng-template>
<ng-template #watchvideo>
<app-watch-video
[video]="video"
[notUnderstoodConcepts]="notUnderstoodConcepts"
[currentMaterial]="currentMaterial"
></app-watch-video>
<button
id="backToList"
pButton
type="button"
(click)="goBack()"
icon="pi pi-chevron-left"
pTooltip="Go back to list"
class="bg-[#0277BD] p-button-rounded"
></button>
</ng-template>
</div>
></app-watch-video>
<button id="backToList"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Guard against null/undefined video in the else block

Prevents runtime errors if video isn’t set when showVideo flips.

-    <ng-template #watchvideo>
-      <app-watch-video
-        [video]="video" 
-        [notUnderstoodConcepts]="notUnderstoodConcepts"
-        [currentMaterial]="currentMaterial"
-      ></app-watch-video>
+    <ng-template #watchvideo>
+      <ng-container *ngIf="video as v">
+        <app-watch-video
+          [video]="v"
+          [notUnderstoodConcepts]="notUnderstoodConcepts"
+          [currentMaterial]="currentMaterial"
+        ></app-watch-video>
+      </ng-container>

Confirm that video is always set before toggling showVideo = true.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ng-template #watchvideo>
<app-watch-video
[video]="video"
[notUnderstoodConcepts]="notUnderstoodConcepts"
(onWatchVideo)="readVideo($event)"
[videoElement]="videoElement"
[currentMaterial]="currentMaterial"
></app-card-video>
</div>
</ng-template>
<ng-template #watchvideo>
<app-watch-video
[video]="video"
[notUnderstoodConcepts]="notUnderstoodConcepts"
[currentMaterial]="currentMaterial"
></app-watch-video>
<button
id="backToList"
pButton
type="button"
(click)="goBack()"
icon="pi pi-chevron-left"
pTooltip="Go back to list"
class="bg-[#0277BD] p-button-rounded"
></button>
</ng-template>
</div>
></app-watch-video>
<ng-template #watchvideo>
<ng-container *ngIf="video as v">
<app-watch-video
[video]="v"
[notUnderstoodConcepts]="notUnderstoodConcepts"
[currentMaterial]="currentMaterial"
></app-watch-video>
</ng-container>
🤖 Prompt for AI Agents
In
webapp/src/app/pages/components/knowledge-graph/videos/card-video-list/card-video-list.component.html
around lines 16-21, the template renders <app-watch-video> in the
else/watchvideo block without guarding against a null/undefined video; either
add a truthy check to the template (e.g., wrap the <app-watch-video> with
*ngIf="video") so the component only renders when video is set, or ensure in the
component TS that you only set showVideo = true after verifying video is
non-null (throw or return early if not); implement one of these guards to
prevent runtime errors when showVideo flips.

Prevents IndexError: list index out of range when a video is invalid/private/deleted or filtered out.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py (2)

33-35: Do not set GOOGLE_APPLICATION_CREDENTIALS in code

Remove inline env mutation; configure via deployment (env/secret manager). Leaving it here risks leaking paths and breaks local/runtime separation.

-    os.environ[
-        "GOOGLE_APPLICATION_CREDENTIALS"
-    ] = "masterthesis-350015-47ab14d0b53b.json"
+    # GOOGLE_APPLICATION_CREDENTIALS must be provided by the environment (CI/K8s/secret store).

176-178: Avoid global string fill; fill per-column with typed defaults

Global "-1" converts numerics to strings.

-            video_data = video_data.fillna("-1")
+            video_data = video_data.fillna({
+                "duration": "0",
+                "views": 0,
+                "description_full": "",
+                "like_count": 0,
+                "channel_title": "",
+                "text": "",
+            })
♻️ Duplicate comments (3)
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py (3)

39-52: Hard-coded API keys committed — rotate immediately and load from env/secret store

All keys must be revoked and removed from source. Load a comma-separated list from env and fail fast if missing. Also drop the unused class-level client to avoid accidental usage.

-    # DEVELOPER_KEY = os.environ.get("YOUTUBE_API_KEY")
-    DEVELOPER_KEY = "AIzaSyBphZOn7EJmPMmZwrB71aepaA5Rbuex9MU"
-    youtube = googleapiclient.discovery.build(
-        api_service_name,
-        api_version,
-        developerKey="AIzaSyClxnNwQ1x34pGioQazLlGxOjO9Fp2GGTY",
-    )
-    DEVELOPER_KEYS = [
-        "AIzaSyD_CGmR_Voq4DIV5okRaR6G8adoe-ZSZsM",
-        "AIzaSyClxnNwQ1x34pGioQazLlGxOjO9Fp2GGTY",
-        "AIzaSyADNntK6m7DbA6eZFYOa9Y8e6IYHykUUFE",
-        "AIzaSyBphZOn7EJmPMmZwrB71aepaA5Rbuex9MU",
-        "AIzaSyB2Wck31LUlgsqI7dgTcC2dMeeVXgb9TDI",
-    ]
+    # Comma-separated API keys from env (e.g., "k1,k2,k3"). Do NOT commit keys.
+    DEVELOPER_KEYS = [k.strip() for k in os.getenv("YOUTUBE_API_KEYS", "").split(",") if k.strip()]
+    if not DEVELOPER_KEYS:
+        raise RuntimeError("YOUTUBE_API_KEYS env var is required (comma-separated YouTube API keys).")

54-90: Retry/backoff logic is broken; fix loop, logging, and quota handling

‘i’ never increments; retries never happen; retry_count == 0 is unreachable; use logger.exception and switch keys on quotaExceeded.

-    def search_youtube_videos(self, developer_keys, query, top_n=50, api_service_name="youtube", api_version="v3"):
+    def search_youtube_videos(self, developer_keys, query, top_n=50, api_service_name="youtube", api_version="v3"):
         """
             Switching YouTube API keys
         """
-        retry_count = 3
-        retry_delay = 5
-        i = 0
-        for key in developer_keys:
-            try:
-                youtube = googleapiclient.discovery.build(api_service_name, api_version, developerKey=key)
-                request = youtube.search().list(
-                    part="snippet",
-                    maxResults=top_n,
-                    type="video",
-                    q=query,
-                    relevanceLanguage="en",
-                )
-                return request.execute(), youtube
-            except (ConnectionAbortedError, ConnectionResetError, timeout) as e:
-                logger.error("Error while getting the videos")
-                logger.error(e)
-                if i == retry_count - 1:
-                    raise  # re-raise the exception if all retries fail
-                delay = retry_delay * (2 ** i)  # use a backoff algorithm to increase the delay
-                time.sleep(delay)
-                logger.info("New Try")
-                if retry_count == 0:
-                    return None, None
-            except HttpError as e:
-                if e.resp.status == 403 and "quota" in str(e):
-                    print(f"Quota exceeded for key: {key}. Trying next key...")
-                else:
-                    raise e
-        raise Exception("All API keys have exceeded their quota.")
+        retry_count = 3
+        retry_delay = 5
+        top_n = min(int(top_n), 50)  # API max
+        developer_keys = developer_keys or self.DEVELOPER_KEYS
+        for key in developer_keys:
+            for i in range(retry_count):
+                try:
+                    youtube = googleapiclient.discovery.build(api_service_name, api_version, developerKey=key)
+                    request = youtube.search().list(
+                        part="snippet",
+                        maxResults=top_n,
+                        type="video",
+                        q=query,
+                        relevanceLanguage="en",
+                    )
+                    return request.execute(), youtube
+                except (ConnectionAbortedError, ConnectionResetError, socket.timeout) as e:
+                    logger.exception("Transient connection error on attempt %d/%d (key ****%s)", i + 1, retry_count, key[-4:])
+                    time.sleep(retry_delay * (2 ** i))
+                    continue
+                except HttpError as e:
+                    status = getattr(getattr(e, "resp", None), "status", None)
+                    msg = str(e).lower()
+                    if status == 403 and ("quotaexceeded" in msg or "dailylimitexceeded" in msg or "quota" in msg):
+                        logger.warning("Quota exceeded for key ****%s; switching key…", key[-4:])
+                        break  # next key
+                    raise
+        raise RuntimeError("All YouTube API keys exhausted or failed.")

Additionally apply outside this range:

# at top of file
import socket  # replace 'from socket import *'

# and remove the star import usage in excepts (done in diff above).

73-76: Log exceptions with traceback; avoid double error lines

Use logger.exception once; also avoid relying on timeout from star-import.

-            except (ConnectionAbortedError, ConnectionResetError, timeout) as e:
-                logger.error("Error while getting the videos")
-                logger.error(e)
+            except (ConnectionAbortedError, ConnectionResetError, socket.timeout) as e:
+                logger.exception("Error while getting the videos")

Outside this range, replace from socket import * with import socket.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 63591bd and 4b97a54.

📒 Files selected for processing (1)
  • coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py (3 hunks)
🧰 Additional context used
🪛 Gitleaks (8.27.2)
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py

47-47: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.

(gcp-api-key)


48-48: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.

(gcp-api-key)


49-49: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.

(gcp-api-key)


50-50: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.

(gcp-api-key)


51-51: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.

(gcp-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build container images / Build image (coursemapper-kg/preprocess)
  • GitHub Check: Build container images / Build image (coursemapper-kg/concept-map)
  • GitHub Check: Build container images / Build image (coursemapper-kg/recommendation)
  • GitHub Check: Build container images / Build image (webapp)
🔇 Additional comments (1)
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py (1)

193-199: Good: exception visibility improved

Catching HttpError and logging with traceback is appropriate.

import logging
from log import LOG
import time
from googleapiclient.errors import HttpError

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Minor: clean imports

Now that HttpError is imported directly, drop import googleapiclient.errors. Also remove duplicate import time (present twice).

-from youtube_transcript_api import YouTubeTranscriptApi
+from youtube_transcript_api import YouTubeTranscriptApi
@@
-import googleapiclient.errors
+# googleapiclient.errors not needed; using HttpError directly
@@
-import time
+# remove duplicate 'import time' if present twice

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around line 15, tidy imports by removing the redundant module import and
duplicate import: delete the now-unnecessary "import googleapiclient.errors"
since HttpError is imported directly from googleapiclient.errors, and remove the
duplicated "import time" so time is only imported once; ensure no other
references depend on the removed module import.

Comment on lines +84 to +88
except HttpError as e:
if e.resp.status == 403 and "quota" in str(e):
print(f"Quota exceeded for key: {key}. Trying next key...")
else:
raise e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace print with structured logging; broaden quota reasons

Use logger and check standard reasons.

-                if e.resp.status == 403 and "quota" in str(e):
-                    print(f"Quota exceeded for key: {key}. Trying next key...")
+                if e.resp.status == 403 and any(r in str(e).lower() for r in ["quotaexceeded", "dailylimitexceeded", "quota"]):
+                    logger.warning("Quota exceeded for key ****%s. Trying next key…", key[-4:])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except HttpError as e:
if e.resp.status == 403 and "quota" in str(e):
print(f"Quota exceeded for key: {key}. Trying next key...")
else:
raise e
except HttpError as e:
if e.resp.status == 403 and any(r in str(e).lower() for r in ["quotaexceeded", "dailylimitexceeded", "quota"]):
logger.warning("Quota exceeded for key ****%s. Trying next key…", key[-4:])
else:
raise e
🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around lines 84-88, replace the plain print with structured logging and broaden
the quota detection: use the module or instance logger (e.g., logger.warning) to
log a clear message that includes the key and exception details (include
exc_info or the exception object), and detect quota errors not only by checking
if "quota" is in the string but also by inspecting standard API error reasons
such as "quotaExceeded", "dailyLimitExceeded" or the HttpError response
body/reason; if a quota-related condition is detected, log the warning and
continue to the next key, otherwise re-raise the exception.

Comment on lines +104 to +106
response, youtube_api_sinlge = self.search_youtube_videos(
developer_keys=self.DEVELOPER_KEYS, query=concepts, top_n=top_n
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Typo: youtube_api_sinlge → youtube_api_single

Fix naming for readability and to avoid propagating typos.

-        response, youtube_api_sinlge = self.search_youtube_videos(
+        response, youtube_api_single = self.search_youtube_videos(
             developer_keys=self.DEVELOPER_KEYS, query=concepts, top_n=top_n
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response, youtube_api_sinlge = self.search_youtube_videos(
developer_keys=self.DEVELOPER_KEYS, query=concepts, top_n=top_n
)
response, youtube_api_single = self.search_youtube_videos(
developer_keys=self.DEVELOPER_KEYS, query=concepts, top_n=top_n
)
🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around lines 104 to 106, there's a typo in the variable name
"youtube_api_sinlge" — rename it to "youtube_api_single" consistently where it's
assigned and anywhere else it's referenced to improve readability and avoid
further typos; update the function return unpacking to use youtube_api_single
and search for other occurrences of the misspelled identifier in the file and
replace them to keep names consistent.

Comment on lines 108 to 111
if len(response["items"]) == 0:
logger.info("No Video found for this input")
return []
else:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard against None/empty responses and return consistent type

Return an empty DataFrame (not list) to keep a stable return type.

-        if len(response["items"]) == 0:
+        if not response or not response.get("items"):
             logger.info("No Video found for this input")
-            return []
+            return pd.DataFrame()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(response["items"]) == 0:
logger.info("No Video found for this input")
return []
else:
if not response or not response.get("items"):
logger.info("No Video found for this input")
return pd.DataFrame()
else:
🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around lines 108 to 111, the code returns an empty list when no videos are
found; change this to return an empty pandas DataFrame to maintain a consistent
return type. Guard against response being None or missing the "items" key before
accessing it (e.g., check response and "items" in response), and return
pandas.DataFrame() when there are no items or response is invalid so callers
always receive a DataFrame.

Comment on lines 95 to +137
for index, id in enumerate(df_ids["id"]):
# try:
# df_snippet["text"][index] = df_snippet["text"][index] + ". " + get_subtitles(id)
# df_snippet["text"][index] = df_snippet["text"][index] + ". " + get_subtitles(id)
# except (NoTranscriptFound, TranscriptsDisabled) as e:
# logger.error("No transcript found in english or transcript disabled for this video "
# "https://www.youtube.com/watch?v={} ".format(id))
# logger.error("No transcript found in english or transcript disabled for this video "
# "https://www.youtube.com/watch?v={} ".format(id))

try:
duration, views, description = self.get_video_details(id)
duration = re.findall(r"\d+", duration)
duration = ":".join(duration)
# print(id, duration, views)
duration_list.append(duration)
view_list.append(views)
description_list.append(description)
except Exception as e:
logger.error("Error while getting the videos details", e)
res = self.get_video_details(youtube_api_sinlge, id)
if res is None:
raise ValueError("No details returned for video id {}".format(id))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Avoid shadowing built-in id and improve error

Rename loop var to video_id; keep error message intact.

-            for index, id in enumerate(df_ids["id"]):
+            for index, video_id in enumerate(df_ids["id"]):
@@
-                    res = self.get_video_details(youtube_api_sinlge, id)
+                    res = self.get_video_details(youtube_api_single, video_id)
-                    if res is None:
-                        raise ValueError("No details returned for video id {}".format(id))
+                    if res is None:
+                        raise ValueError("No details returned for video id {}".format(video_id))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for index, id in enumerate(df_ids["id"]):
# try:
# df_snippet["text"][index] = df_snippet["text"][index] + ". " + get_subtitles(id)
# df_snippet["text"][index] = df_snippet["text"][index] + ". " + get_subtitles(id)
# except (NoTranscriptFound, TranscriptsDisabled) as e:
# logger.error("No transcript found in english or transcript disabled for this video "
# "https://www.youtube.com/watch?v={} ".format(id))
# logger.error("No transcript found in english or transcript disabled for this video "
# "https://www.youtube.com/watch?v={} ".format(id))
try:
duration, views, description = self.get_video_details(id)
duration = re.findall(r"\d+", duration)
duration = ":".join(duration)
# print(id, duration, views)
duration_list.append(duration)
view_list.append(views)
description_list.append(description)
except Exception as e:
logger.error("Error while getting the videos details", e)
res = self.get_video_details(youtube_api_sinlge, id)
if res is None:
raise ValueError("No details returned for video id {}".format(id))
for index, video_id in enumerate(df_ids["id"]):
# try:
# df_snippet["text"][index] = df_snippet["text"][index] + ". " + get_subtitles(id)
# except (NoTranscriptFound, TranscriptsDisabled) as e:
# logger.error("No transcript found in english or transcript disabled for this video "
# "https://www.youtube.com/watch?v={} ".format(id))
try:
- res = self.get_video_details(youtube_api_sinlge, id)
- if res is None:
res = self.get_video_details(youtube_api_single, video_id)
if res is None:
raise ValueError("No details returned for video id {}".format(video_id))
🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around lines 127 to 137, the loop variable currently named "id" shadows Python's
built-in id() and should be renamed to "video_id"; update the for statement and
every reference inside the loop (e.g., pass video_id to get_video_details and to
the error message string.format) so behavior and the existing error message
remain the same while avoiding built-in shadowing.

Comment on lines +141 to +153
# Keep your duration normalization
duration = re.findall(r"\d+", str(duration))
duration = ":".join(duration)
except Exception:
# Fix logging formatter error and keep same message semantics
logger.exception("Error while getting the videos details for id %s", id)
# Append safe defaults to keep list lengths aligned with df_ids
duration = "0"
views = 0
description = ""
like_count = 0
channel_title = ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Normalize duration properly; keep numeric types for counts

Parse ISO-8601 with isodate if available; coerce counts to ints.

-                    duration = re.findall(r"\d+", str(duration))
-                    duration = ":".join(duration)
+                    # Prefer true ISO-8601 parsing if available; fallback kept
+                    try:
+                        import isodate
+                        duration = int(isodate.parse_duration(str(duration)).total_seconds())
+                    except Exception:
+                        duration = ":".join(re.findall(r"\d+", str(duration)))
@@
-                    views = 0
+                    views = 0
@@
-                    like_count = 0
+                    like_count = 0

And when appending (below), coerce to numeric (see next comment).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Keep your duration normalization
duration = re.findall(r"\d+", str(duration))
duration = ":".join(duration)
except Exception:
# Fix logging formatter error and keep same message semantics
logger.exception("Error while getting the videos details for id %s", id)
# Append safe defaults to keep list lengths aligned with df_ids
duration = "0"
views = 0
description = ""
like_count = 0
channel_title = ""
# Keep your duration normalization
- duration = re.findall(r"\d+", str(duration))
# Prefer true ISO-8601 parsing if available; fallback to regex join
try:
import isodate
duration = int(isodate.parse_duration(str(duration)).total_seconds())
except Exception:
duration = ":".join(re.findall(r"\d+", str(duration)))
except Exception:
# Fix logging formatter error and keep same message semantics
logger.exception("Error while getting the videos details for id %s", id)
# Append safe defaults to keep list lengths aligned with df_ids
duration = "0"
views = 0
description = ""
like_count = 0
channel_title = ""
🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around lines 141 to 153, the duration and numeric fields are being treated as
strings and not normalized consistently; update the exception handling and
normal parsing to (1) parse ISO-8601 durations using isodate.parse_duration
(fall back to existing regex only if isodate is unavailable) and normalize to a
canonical "H:MM:SS" or "MM:SS" string for duration, (2) ensure counts like views
and like_count are coerced to int (use int(...) with safe default 0) and
description/channel_title stay as strings, and (3) keep the existing
logger.exception call but maintain its safe formatting; after fixing here,
ensure any append/population logic below appends ints for numeric fields and the
normalized duration string so list lengths and types remain consistent.

Comment on lines +163 to +169
# Assign lists (same behavior as before, now lengths aligned)
video_data["duration"] = pd.Series(duration_list)
video_data["views"] = pd.Series(view_list)
video_data["description_full"] = pd.Series(description_list)
video_data["like_count"] = pd.Series(like_count_list)
video_data["channel_title"] = pd.Series(channel_title_list)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Preserve numeric dtypes for views/likes; avoid implicit object dtype

Cast with to_numeric and nullable Int64.

-            video_data["views"] = pd.Series(view_list)
+            video_data["views"] = pd.to_numeric(pd.Series(view_list), errors="coerce").fillna(0).astype("Int64")
@@
-            video_data["like_count"] = pd.Series(like_count_list)
+            video_data["like_count"] = pd.to_numeric(pd.Series(like_count_list), errors="coerce").fillna(0).astype("Int64")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Assign lists (same behavior as before, now lengths aligned)
video_data["duration"] = pd.Series(duration_list)
video_data["views"] = pd.Series(view_list)
video_data["description_full"] = pd.Series(description_list)
video_data["like_count"] = pd.Series(like_count_list)
video_data["channel_title"] = pd.Series(channel_title_list)
# Assign lists (same behavior as before, now lengths aligned)
video_data["duration"] = pd.Series(duration_list)
video_data["views"] = pd.to_numeric(pd.Series(view_list), errors="coerce").fillna(0).astype("Int64")
video_data["description_full"] = pd.Series(description_list)
video_data["like_count"] = pd.to_numeric(pd.Series(like_count_list), errors="coerce").fillna(0).astype("Int64")
video_data["channel_title"] = pd.Series(channel_title_list)
🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around lines 163 to 169, assigning views and like_count directly from lists
produces object dtype; convert these Series to numeric using
pandas.to_numeric(..., errors='coerce') and then cast to the nullable Integer
dtype .astype("Int64") so missing/non-numeric values become <NA> instead of
strings; update the assignment for video_data["views"] and
video_data["like_count"] to build Series from the lists, apply to_numeric with
errors='coerce', and then .astype("Int64").

Comment on lines 171 to 173
video_data["text"] = pd.DataFrame(
video_data["title"] + ". " + video_data["description"]
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Assign Series, not DataFrame, when building text

Avoid wrapping in DataFrame; also coerce to str to be safe.

-            video_data["text"] = pd.DataFrame(
-                video_data["title"] + ". " + video_data["description"]
-            )
+            video_data["text"] = video_data["title"].astype(str) + ". " + video_data["description"].astype(str)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
video_data["text"] = pd.DataFrame(
video_data["title"] + ". " + video_data["description"]
)
video_data["text"] = video_data["title"].astype(str) + ". " + video_data["description"].astype(str)
🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around lines 171 to 173, the code wraps the concatenated title and description
in a DataFrame; instead assign a pandas Series and ensure both title and
description are coerced to strings. Replace the DataFrame construction with a
Series (e.g., pd.Series((video_data["title"].astype(str) + ". " +
video_data["description"].astype(str)))) so text is a Series of strings rather
than a DataFrame.

Comment on lines +180 to +186
def get_video_details(self, youtube_api_sinlge, video_id):
# print("get_video_details for id -------------------- ", video_id)
try:
duration = (
r["items"][0]["contentDetails"]["duration"]
if r["items"][0]["contentDetails"]["duration"]
else 0
)
views = (
r["items"][0]["statistics"]["viewCount"]
if r["items"][0]["statistics"]["viewCount"]
else 0
)
description = (
r["items"][0]["snippet"]["description"]
if r["items"][0]["snippet"]["description"]
else ""
)

except Exception as e:
print("---------------------------------------")
print(e)
# The number of views are not present for some videos and this leads to an exception. For this
# reason a default value of 0 views will be given that video.
views = 0
duration = (
r["items"][0]["contentDetails"]["duration"]
if r["items"][0]["contentDetails"]["duration"]
else 0
)
description = (
r["items"][0]["snippet"]["description"]
if r["items"][0]["snippet"]["description"]
else ""
r = (
# self.youtube.videos()
youtube_api_sinlge.videos()
.list(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Typo and minimize API fields

Fix param name and narrow fields to what you actually read.

-    def get_video_details(self, youtube_api_sinlge, video_id):
+    def get_video_details(self, youtube_api_single, video_id):
@@
-                youtube_api_sinlge.videos()
+                youtube_api_single.videos()
                 .list(
-                    part="snippet,statistics,contentDetails",
+                    part="snippet,statistics,contentDetails",
                     id=video_id,
-                    fields="items(statistics,contentDetails(duration),snippet)",
+                    fields="items(statistics(viewCount,likeCount),contentDetails(duration),snippet(description,channelTitle))",
                 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_video_details(self, youtube_api_sinlge, video_id):
# print("get_video_details for id -------------------- ", video_id)
try:
duration = (
r["items"][0]["contentDetails"]["duration"]
if r["items"][0]["contentDetails"]["duration"]
else 0
)
views = (
r["items"][0]["statistics"]["viewCount"]
if r["items"][0]["statistics"]["viewCount"]
else 0
)
description = (
r["items"][0]["snippet"]["description"]
if r["items"][0]["snippet"]["description"]
else ""
)
except Exception as e:
print("---------------------------------------")
print(e)
# The number of views are not present for some videos and this leads to an exception. For this
# reason a default value of 0 views will be given that video.
views = 0
duration = (
r["items"][0]["contentDetails"]["duration"]
if r["items"][0]["contentDetails"]["duration"]
else 0
)
description = (
r["items"][0]["snippet"]["description"]
if r["items"][0]["snippet"]["description"]
else ""
r = (
# self.youtube.videos()
youtube_api_sinlge.videos()
.list(
def get_video_details(self, youtube_api_single, video_id):
# print("get_video_details for id -------------------- ", video_id)
try:
r = (
# self.youtube.videos()
youtube_api_single.videos()
.list(
part="snippet,statistics,contentDetails",
id=video_id,
fields="items(statistics(viewCount,likeCount),contentDetails(duration),snippet(description,channelTitle))",
)
)
# …rest of method…

Comment on lines +210 to +217
# Safe gets with defaults (keep same return semantics)
duration = cd.get("duration") if cd.get("duration") else 0
views = st.get("viewCount") if st.get("viewCount") else 0
description = sn.get("description") if sn.get("description") else ""
like_count = st.get("likeCount") if st.get("likeCount") else 0
channel_title = sn.get("channelTitle") if sn.get("channelTitle") else ""

return duration, views, description, like_count, channel_title

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Return typed values; default safely

Coerce counts to int and keep empty strings for text fields.

-        duration = cd.get("duration") if cd.get("duration") else 0
-        views = st.get("viewCount") if st.get("viewCount") else 0
-        description = sn.get("description") if sn.get("description") else ""
-        like_count = st.get("likeCount") if st.get("likeCount") else 0
-        channel_title = sn.get("channelTitle") if sn.get("channelTitle") else ""
+        duration = cd.get("duration") or 0
+        views = int(st.get("viewCount") or 0)
+        description = sn.get("description") or ""
+        like_count = int(st.get("likeCount") or 0)
+        channel_title = sn.get("channelTitle") or ""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Safe gets with defaults (keep same return semantics)
duration = cd.get("duration") if cd.get("duration") else 0
views = st.get("viewCount") if st.get("viewCount") else 0
description = sn.get("description") if sn.get("description") else ""
like_count = st.get("likeCount") if st.get("likeCount") else 0
channel_title = sn.get("channelTitle") if sn.get("channelTitle") else ""
return duration, views, description, like_count, channel_title
# Safe gets with defaults (keep same return semantics)
duration = cd.get("duration") or 0
views = int(st.get("viewCount") or 0)
description = sn.get("description") or ""
like_count = int(st.get("likeCount") or 0)
channel_title = sn.get("channelTitle") or ""
return duration, views, description, like_count, channel_title
🤖 Prompt for AI Agents
In
coursemapper-kg/recommendation/app/services/course_materials/recommendation/youtube_service.py
around lines 210 to 217, the numeric fields are returned as-is and may be
strings or None; coerce the count/duration fields to ints and preserve empty
strings for text fields by using safe defaults. Replace the current assignments
with expressions that use the .get(... ) or fallback and coerce to int (e.g.
int(cd.get("duration") or 0), int(st.get("viewCount") or 0),
int(st.get("likeCount") or 0)) and keep description and channel_title as
sn.get("description") or "" and sn.get("channelTitle") or "" so return types are
int, int, str, int, str.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
coursemapper-kg/recommendation/app/services/course_materials/GCN/gcn.py (2)

22-24: Don't disable encryption for Neo4j in production.

encrypted=False exposes data in transit. Make it configurable and default to True (or use a neo4j+s URI).

Apply:

-        self.driver = GraphDatabase.driver(neo4j_uri,
-                                           auth=(neo4j_user, neo4j_pass),
-                                           encrypted=False)
+        self.driver = GraphDatabase.driver(
+            neo4j_uri,
+            auth=(neo4j_user, neo4j_pass),
+            encrypted=getattr(Config, "NEO4J_ENCRYPTED", True),
+        )

87-97: Batch Neo4j writes via UNWIND to avoid N round-trips.

One query per node will be slow at scale.

Apply:

-        with self.driver.session() as session:
-            for i in range(final_embeddings.shape[0]):
-                id = idx[i]
-                f_embedding = final_embeddings[i]
-                embedding = ",".join(str(i) for i in f_embedding)
-                # Find a node in neo4j by its original id and save its final embedding into its "final_embedding" property
-                result = session.run("""MATCH (n) WHERE n.cid= $id or n.sid= $id
-                        set n.final_embedding = $embedding RETURN n""",
-                    id=id,
-                    embedding=embedding)
+        rows = [
+            {"id": str(idx[i]), "embedding": ",".join(map(str, final_embeddings[i]))}
+            for i in range(final_embeddings.shape[0])
+        ]
+        with self.driver.session() as session:
+            session.run(
+                """
+                UNWIND $rows AS row
+                MATCH (n) WHERE n.cid = row.id OR n.sid = row.id
+                SET n.final_embedding = row.embedding
+                """,
+                rows=rows,
+            )
♻️ Duplicate comments (1)
coursemapper-kg/recommendation/app/services/course_materials/GCN/gcn.py (1)

37-37: Prefer debug-level, shape-only logs over commenting out.

Same feedback as before; keep lightweight observability.

Apply:

-        # logger.info(idx_features.shape[0])
+        logger.debug("idx_features count: %d", idx_features.shape[0])
@@
-        # logger.info(features.A)
+        logger.debug("features shape: %s", features.shape)
@@
-        # logger.info(adj.A)
+        logger.debug("adj shape: %s", adj.shape)
@@
-        # logger.info(final_embeddings)
+        logger.debug("final_embeddings shape: %s", final_embeddings.shape)

Also applies to: 41-41, 74-74, 83-83

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b97a54 and 508ee8c.

📒 Files selected for processing (1)
  • coursemapper-kg/recommendation/app/services/course_materials/GCN/gcn.py (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build container images / Build image (coursemapper-kg/preprocess)
  • GitHub Check: Build container images / Build image (webapp)
  • GitHub Check: Build container images / Build image (coursemapper-kg/concept-map)
  • GitHub Check: Build container images / Build image (coursemapper-kg/recommendation)
🔇 Additional comments (2)
coursemapper-kg/recommendation/app/services/course_materials/GCN/gcn.py (2)

69-69: No-op formatting change.

Safe to keep or drop per project style.


126-127: LGTM; where= supported since NumPy 1.17 and pinned version ==1.26.4 satisfies the requirement.

@@ -66,11 +66,12 @@ def load_data(self):
dtype=np.float32,
)
adj = np.around(adj, 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

np.around densifies sparse matrices — major perf/memory bug.

Rounding the whole sparse matrix converts it to dense; use .data instead.

Apply:

-        adj = np.around(adj, 2)
+        # Round only nonzero weights to keep sparsity
+        adj = adj.tocoo()
+        adj.data = np.around(adj.data, 2)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
adj = np.around(adj, 2)
# Round only nonzero weights to keep sparsity
adj = adj.tocoo()
adj.data = np.around(adj.data, 2)
🤖 Prompt for AI Agents
In coursemapper-kg/recommendation/app/services/course_materials/GCN/gcn.py
around line 68, calling np.around on the entire sparse adjacency matrix
densifies it and causes major perf/memory issues; instead apply rounding only to
the sparse data array (the .data attribute) and keep the matrix as a sparse
type, i.e., update the matrix's internal data with rounded values and do not
convert or recreate a dense matrix.

# matrix plus its unit matrix and transpose matrix to obtain the complete adjacency matrix
adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)

adj = self.normalize(adj) + sp.eye(adj.shape[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add self-loops before normalization (GCN convention).

Current order changes weights vs. Kipf & Welling’s  = D̂^{-1/2}(A+I)D̂^{-1/2}.

Apply:

-        adj = self.normalize(adj) + sp.eye(adj.shape[0])
+        # Add self-loops, then normalize
+        adj = self.normalize(adj + sp.eye(adj.shape[0]))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
adj = self.normalize(adj) + sp.eye(adj.shape[0])
# Add self-loops, then normalize
adj = self.normalize(adj + sp.eye(adj.shape[0]))
🤖 Prompt for AI Agents
In coursemapper-kg/recommendation/app/services/course_materials/GCN/gcn.py
around line 73 the code adds self-loops after calling normalize which changes
the resulting weights versus the standard GCN convention; move the addition of
the identity so you add self-loops to adj before calling self.normalize (i.e.,
compute adj_with_loops = adj + sp.eye(adj.shape[0]) then call
self.normalize(adj_with_loops)), and ensure the normalize function computes
degrees and performs symmetric normalization on that augmented matrix.

dependabot Bot and others added 4 commits April 14, 2026 07:53
Bumps nginxinc/nginx-unprivileged from 1.29.1-alpine to 1.29.3-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-version: 1.29.3-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps nginxinc/nginx-unprivileged from 1.29.0-alpine to 1.29.3-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-version: 1.29.3-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Update user agent in the response of search query + DNS API

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
webapp/Dockerfile (1)

20-32: ⚠️ Potential issue | 🟡 Minor

Add HEALTHCHECK to the runtime image.

The final NGINX stage exposes port 4200 but has no healthcheck, reducing failure detection in container orchestration.

Suggested patch
 FROM nginxinc/nginx-unprivileged:1.29.3-alpine
@@
 ENV NGINX_ENTRYPOINT_QUIET_LOGS=1
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+  CMD wget -q -O /dev/null http://127.0.0.1:4200/ || exit 1
 EXPOSE 4200
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/Dockerfile` around lines 20 - 32, The Dockerfile final stage (FROM
nginxinc/nginx-unprivileged:1.29.3-alpine) exposes port 4200 but lacks a
HEALTHCHECK; add a HEALTHCHECK instruction after ENV NGINX_ENTRYPOINT_QUIET_LOGS
and before or after EXPOSE 4200 that probes the running nginx (for example an
HTTP GET against localhost:4200 or the nginx status endpoint) with sensible
interval/retries/timeout settings so the container runtime can detect unhealthy
instances; ensure the check runs as the unprivileged user (USER 101) or uses a
simple curl/wget shell command available in the image, and reference the
existing ENV NGINX_ENTRYPOINT_QUIET_LOGS and EXPOSE 4200 in the commit so
reviewers can locate the change.
proxy/Dockerfile (1)

1-7: ⚠️ Potential issue | 🟡 Minor

Add a container HEALTHCHECK for runtime reliability.

There is no healthcheck in this image, so orchestrators cannot detect unhealthy NGINX workers.

Suggested patch
 FROM nginxinc/nginx-unprivileged:1.29.3-alpine
 
 USER root
 COPY --link ./nginx/conf.d/* /etc/nginx/conf.d/
 
+HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
+  CMD wget -q -O /dev/null http://127.0.0.1:8000/ || exit 1
+
 EXPOSE 8000
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/Dockerfile` around lines 1 - 7, Add a Docker HEALTHCHECK to the
Dockerfile so orchestrators can detect unhealthy NGINX workers: update the
Dockerfile (the image line FROM nginxinc/nginx-unprivileged:1.29.3-alpine / COPY
and EXPOSE 8000 context) to include a HEALTHCHECK instruction (using CMD-SHELL)
that probes localhost:8000 (e.g., curl/wget --fail or ncat) with sensible
parameters (interval, timeout, retries); place it after the COPY/EXPOSE lines
and ensure it exits non-zero on failure so the container is marked unhealthy.
coursemapper-kg/concept-map/src/services/annotation.py (1)

11-29: ⚠️ Potential issue | 🟠 Major

Inconsistent DBpedia Spotlight endpoint configuration across services.

The concept-map service uses environment variable configuration with a DNS override fallback mechanism, but the recommendation service modules hardcode the same DBpedia Spotlight endpoint in four separate locations without any configuration or DNS handling:

  1. coursemapper-kg/recommendation/app/services/course_materials/kwp_extraction/dbpedia/dataAvailability.py
  2. coursemapper-kg/recommendation/app/services/course_materials/kwp_extraction/dbpedia/concept_tagging.py
  3. coursemapper-kg/recommendation/app/services/course_materials/kwp_extraction/dbpedia/concept_tagging_top_down.py
  4. coursemapper-kg/recommendation/app/services/course_materials/kwp_extraction/dbpedia/concept_tagging Paul.py

All use:

self.url = "https://api.dbpedia-spotlight.org/%s/annotate" % lang

Meanwhile, concept-map uses Config.DBPEDIA_SPOTLIGHT_URL (from environment) and includes a DNS override mechanism to resolve api.dbpedia-spotlight.org to 134.155.98.34.

Impact: This creates a configuration management burden—any endpoint change requires editing multiple hardcoded locations in recommendation services, while concept-map uses environment configuration. The DNS override mechanism in concept-map suggests a specific deployment requirement (firewall, load balancer, specific IP) that recommendation services cannot benefit from.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coursemapper-kg/concept-map/src/services/annotation.py` around lines 11 - 29,
The recommendation modules hardcode the DBpedia Spotlight endpoint in multiple
places (self.url = "https://api.dbpedia-spotlight.org/%s/annotate" % lang)
instead of using the centralized configuration and DNS override used in
concept-map; update each recommendation class that sets self.url (in
dataAvailability.py, concept_tagging.py, concept_tagging_top_down.py, and
concept_tagging Paul.py) to read the endpoint from the shared configuration (use
Config.DBPEDIA_SPOTLIGHT_URL or an equivalent environment-backed config value)
and remove the hardcoded literal, and ensure any deployment-specific DNS
override logic (the dns_cache/override_dns/new_getaddrinfo mechanism) is
provided centrally at application startup so the recommendation services inherit
the same DNS behavior rather than implementing ad-hoc URLs.
♻️ Duplicate comments (1)
proxy/Dockerfile (1)

2-2: ⚠️ Potential issue | 🟠 Major

Pin the NGINX image to a digest, not a floating tag.

Line 2 still uses a mutable tag, which weakens reproducibility and supply-chain control.

#!/bin/bash
# Verify and retrieve the digest for nginxinc/nginx-unprivileged:1.29.3-alpine
set -euo pipefail

token="$(curl -fsSL 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:nginxinc/nginx-unprivileged:pull' | jq -r '.token')"

curl -fsSI \
  -H "Authorization: Bearer ${token}" \
  -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
  "https://registry-1.docker.io/v2/nginxinc/nginx-unprivileged/manifests/1.29.3-alpine" \
  | awk 'BEGIN{IGNORECASE=1} /docker-content-digest/ {print $0}'

Expected result: a docker-content-digest header you can pin in FROM nginxinc/nginx-unprivileged@sha256:....

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/Dockerfile` at line 2, Replace the floating image tag in the
Dockerfile's FROM instruction (currently "FROM
nginxinc/nginx-unprivileged:1.29.3-alpine") with the corresponding immutable
digest; obtain the image digest using the Docker registry manifest endpoint (or
the provided verification snippet) and update the line to use "FROM
nginxinc/nginx-unprivileged@sha256:..." so the build is pinned to the specific
content-addressable image.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Line 14: The workflow uses the mutable tag actions/checkout@v6 which is a
supply-chain risk; replace that tag with the full immutable commit SHA for the
v6 release (i.e., change uses: actions/checkout@v6 to uses:
actions/checkout@<FULL_COMMIT_SHA>) and add a trailing comment with the
human-friendly tag/version (e.g., # v6.x.y) for maintainability so reviewers can
see which release the SHA corresponds to.

In `@coursemapper-kg/concept-map/src/services/annotation.py`:
- Line 29: Apply the DNS override consistently by centralizing the DBpedia
Spotlight endpoint: create a shared configuration (e.g.,
DBPEDIA_SPOTLIGHT_HOST/DBPEDIA_SPOTLIGHT_URL) or shared init module that calls
override_dns('api.dbpedia-spotlight.org','134.155.98.34') and export the
canonical endpoint, then update annotation.py to import that shared init
(instead of calling override_dns inline) and change the other service that
hardcodes "https://api.dbpedia-spotlight.org/%s/annotate" (see
course_materials/.../kwp_extraction/dbpedia/concept_tagging.py) to use the
shared DBPEDIA_SPOTLIGHT_URL or host variable so all services (including
AnnotationService) resolve via the same override/config.

In `@webapp/Dockerfile`:
- Line 2: Replace the mutable base image tags in the Dockerfile with the
provided immutable digests: update the build stage FROM reference (currently
"node:24.0-slim" in the FROM line used by the build stage) to the pinned digest
"node:24.0-slim@sha256:083430e81f23ca4f309c6de17614d20706ddd544b2adc71fb9fdd86e2371360a",
and update the final runtime FROM reference (currently
"nginxinc/nginx-unprivileged:1.29.3-alpine") to the pinned digest
"nginxinc/nginx-unprivileged:1.29.3-alpine@sha256:5aea7cc516b419e3526f47dd1531be31a56a046cfe44754d94f9383e13e2ee99"
so rebuilds are deterministic.

In `@webserver/src/controllers/knowledgeGraph.controller.js`:
- Around line 515-522: Replace the dead commented axios call and update the
User-Agent header used in the axios.get request inside the code that performs
the Wikimedia API fetch: remove the leftover "// const response = await
axios.get(url);" line and change "User-Agent": "CourseMapper
(coursemapper@example.com)" to a real contact string (project URL or maintainer
email) per Wikimedia policy (e.g., "CourseMapper/1.0
(+https://yourproject.example.com)" or include a real maintainer email) in the
axios.get call so Wikimedia can contact the operator.

---

Outside diff comments:
In `@coursemapper-kg/concept-map/src/services/annotation.py`:
- Around line 11-29: The recommendation modules hardcode the DBpedia Spotlight
endpoint in multiple places (self.url =
"https://api.dbpedia-spotlight.org/%s/annotate" % lang) instead of using the
centralized configuration and DNS override used in concept-map; update each
recommendation class that sets self.url (in dataAvailability.py,
concept_tagging.py, concept_tagging_top_down.py, and concept_tagging Paul.py) to
read the endpoint from the shared configuration (use
Config.DBPEDIA_SPOTLIGHT_URL or an equivalent environment-backed config value)
and remove the hardcoded literal, and ensure any deployment-specific DNS
override logic (the dns_cache/override_dns/new_getaddrinfo mechanism) is
provided centrally at application startup so the recommendation services inherit
the same DNS behavior rather than implementing ad-hoc URLs.

In `@proxy/Dockerfile`:
- Around line 1-7: Add a Docker HEALTHCHECK to the Dockerfile so orchestrators
can detect unhealthy NGINX workers: update the Dockerfile (the image line FROM
nginxinc/nginx-unprivileged:1.29.3-alpine / COPY and EXPOSE 8000 context) to
include a HEALTHCHECK instruction (using CMD-SHELL) that probes localhost:8000
(e.g., curl/wget --fail or ncat) with sensible parameters (interval, timeout,
retries); place it after the COPY/EXPOSE lines and ensure it exits non-zero on
failure so the container is marked unhealthy.

In `@webapp/Dockerfile`:
- Around line 20-32: The Dockerfile final stage (FROM
nginxinc/nginx-unprivileged:1.29.3-alpine) exposes port 4200 but lacks a
HEALTHCHECK; add a HEALTHCHECK instruction after ENV NGINX_ENTRYPOINT_QUIET_LOGS
and before or after EXPOSE 4200 that probes the running nginx (for example an
HTTP GET against localhost:4200 or the nginx status endpoint) with sensible
interval/retries/timeout settings so the container runtime can detect unhealthy
instances; ensure the check runs as the unprivileged user (USER 101) or uses a
simple curl/wget shell command available in the image, and reference the
existing ENV NGINX_ENTRYPOINT_QUIET_LOGS and EXPOSE 4200 in the commit so
reviewers can locate the change.

---

Duplicate comments:
In `@proxy/Dockerfile`:
- Line 2: Replace the floating image tag in the Dockerfile's FROM instruction
(currently "FROM nginxinc/nginx-unprivileged:1.29.3-alpine") with the
corresponding immutable digest; obtain the image digest using the Docker
registry manifest endpoint (or the provided verification snippet) and update the
line to use "FROM nginxinc/nginx-unprivileged@sha256:..." so the build is pinned
to the specific content-addressable image.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2bc197af-7679-4e4f-bf78-3d51846d0e78

📥 Commits

Reviewing files that changed from the base of the PR and between 508ee8c and 2e9eb15.

📒 Files selected for processing (7)
  • .github/workflows/build.yml
  • .github/workflows/release.yml
  • coursemapper-kg/concept-map/src/graph_db.py
  • coursemapper-kg/concept-map/src/services/annotation.py
  • proxy/Dockerfile
  • webapp/Dockerfile
  • webserver/src/controllers/knowledgeGraph.controller.js

Comment thread .github/workflows/release.yml Outdated
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that v6 exists and retrieve the exact commit SHA to pin.
# Read-only commands only.

set -euo pipefail

echo "Latest checkout tags (top 20):"
curl -fsSL "https://api.github.com/repos/actions/checkout/tags?per_page=20" | jq -r '.[].name'

echo
echo "Resolve v6 to commit SHA:"
curl -fsSL "https://api.github.com/repos/actions/checkout/git/ref/tags/v6" | jq -r '.object.sha'

Repository: ude-soco/CourseMapper-webserver

Length of output: 172


🏁 Script executed:

#!/bin/bash

# Check if the workflow file exists and examine line 14 and surrounding context
if [ -f ".github/workflows/release.yml" ]; then
  echo "=== Workflow file found ==="
  echo "Lines 1-20 of .github/workflows/release.yml:"
  head -20 ".github/workflows/release.yml" | cat -n
  echo
  echo "=== Full file (checking for permissions declarations) ==="
  cat ".github/workflows/release.yml"
else
  echo "Workflow file not found at .github/workflows/release.yml"
  # List what's in .github/workflows if it exists
  if [ -d ".github/workflows" ]; then
    echo "Contents of .github/workflows/:"
    ls -la ".github/workflows/"
  fi
fi

Repository: ude-soco/CourseMapper-webserver

Length of output: 1626


🌐 Web query:

actions/checkout v6 GitHub Actions tag

💡 Result:

The GitHub Actions actions/checkout v6 tag refers to the major version 6 of the official checkout action, used to check out repositories in workflows. The latest release under v6 is v6.0.2, released on January 9, 2026. Key details: - Tag: actions/checkout@v6 or specifically actions/checkout@v6.0.2 - Main changes in v6.0.0: Improved credential security by storing persist-credentials in $RUNNER_TEMP instead of .git/config (requires Actions Runner v2.329.0+ for Docker containers). No workflow changes needed for most users; git commands like fetch/push still work. - Usage example: - uses: actions/checkout@v6 with: repository: 'owner/repo' # optional ref: 'branch-or-tag' # optional - Requires Node.js 24 runtime support. - Official repository: https://github.com/actions/checkout (50 releases total, latest v6.0.2). As of 2026-04-21, v6 is the current major version.

Citations:


🌐 Web query:

GitHub Actions security best practices pin commit SHA mutable version tags

💡 Result:

GitHub Actions security best practices emphasize pinning actions to full-length commit SHAs instead of mutable version tags (like @v4 or @main) to prevent supply chain attacks where tags can be force-pushed to malicious commits. Key practices: - Use full 40-character SHA: - uses: actions/checkout@11bd719 # v4.2.2 Avoid: - uses: actions/checkout@v4 (mutable tag) - Verify SHA from original repository, not forks. - Add version comment for readability. - Prioritize high-risk/third-party actions. - Use Dependabot/Renovate for automated SHA updates on new releases. - Enable organization/enterprise policies to enforce SHA pinning (fails unpinned workflows). - Pin sub-actions in custom actions. - Combine with least-privilege GITHUB_TOKEN, OIDC for cloud access, code scanning. This ensures immutability: SHAs cannot change without content alteration, mitigating risks shown in real attacks like tj-actions/changed-files (CVE-2025-30066). Official GitHub Docs: Pinning to full SHA is the only immutable reference method.

Citations:


Pin actions/checkout to an immutable commit SHA, not a floating major tag.

Line 14 uses @v6, a mutable major version tag. In workflows with contents: write permissions, this creates supply-chain risk—tags can be force-pushed to malicious commits. Pin to the full commit SHA of the v6 release and include the version in a comment for maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml at line 14, The workflow uses the mutable tag
actions/checkout@v6 which is a supply-chain risk; replace that tag with the full
immutable commit SHA for the v6 release (i.e., change uses: actions/checkout@v6
to uses: actions/checkout@<FULL_COMMIT_SHA>) and add a trailing comment with the
human-friendly tag/version (e.g., # v6.x.y) for maintainability so reviewers can
see which release the SHA corresponds to.

socket.getaddrinfo = new_getaddrinfo

override_dns('api.dbpedia-spotlight.org', '134.155.95.34')
override_dns('api.dbpedia-spotlight.org', '134.155.98.34')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the new DBpedia Spotlight IP and service availability

echo "=== Checking DNS resolution for api.dbpedia-spotlight.org ==="
nslookup api.dbpedia-spotlight.org || dig api.dbpedia-spotlight.org

echo -e "\n=== Testing connectivity to new IP 134.155.98.34 ==="
curl -I --connect-timeout 10 https://134.155.98.34/ 2>&1 || echo "Direct IP connection failed"

echo -e "\n=== Testing DBpedia Spotlight API on new IP ==="
curl -s --connect-timeout 10 \
  -H "Accept: application/json" \
  -H "Host: api.dbpedia-spotlight.org" \
  "https://134.155.98.34/en/annotate?text=Berlin&confidence=0.5&support=20" 2>&1

echo -e "\n=== Testing DBpedia Spotlight API via hostname ==="
curl -s --connect-timeout 10 \
  -H "Accept: application/json" \
  "https://api.dbpedia-spotlight.org/en/annotate?text=Berlin&confidence=0.5&support=20" 2>&1

Repository: ude-soco/CourseMapper-webserver

Length of output: 1644


🏁 Script executed:

# Search for DBpedia Spotlight references in the codebase
rg -i "dbpedia" --type py -B2 -A2

Repository: ude-soco/CourseMapper-webserver

Length of output: 50387


🏁 Script executed:

# Search for other DNS override mechanisms
rg "override_dns|socket\.getaddrinfo" --type py -B2 -A2

Repository: ude-soco/CourseMapper-webserver

Length of output: 1416


🏁 Script executed:

# Look for other services that might use the DBpedia API
fd -e py -type f | xargs rg "dbpedia-spotlight" -l

Repository: ude-soco/CourseMapper-webserver

Length of output: 784


Apply DNS override consistently across all DBpedia Spotlight services.

The IP address change to 134.155.98.34 is functional and the service is accessible. However, the DNS override mechanism is applied only in this service, while other recommendation services in the codebase (e.g., coursemapper-kg/recommendation/app/services/course_materials/kwp_extraction/dbpedia/concept_tagging.py) hardcode the full URL without the DNS override. This creates an inconsistency where different services may resolve to different IPs:

  • AnnotationService (this file): Uses override_dns() to map api.dbpedia-spotlight.org134.155.98.34
  • Recommendation services: Hardcode https://api.dbpedia-spotlight.org/%s/annotate with default DNS resolution

Consider either:

  1. Centralizing the DNS override in a shared initialization module imported by all services
  2. Using environment variables or configuration to define the DBpedia Spotlight endpoint consistently across all services
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coursemapper-kg/concept-map/src/services/annotation.py` at line 29, Apply the
DNS override consistently by centralizing the DBpedia Spotlight endpoint: create
a shared configuration (e.g., DBPEDIA_SPOTLIGHT_HOST/DBPEDIA_SPOTLIGHT_URL) or
shared init module that calls
override_dns('api.dbpedia-spotlight.org','134.155.98.34') and export the
canonical endpoint, then update annotation.py to import that shared init
(instead of calling override_dns inline) and change the other service that
hardcodes "https://api.dbpedia-spotlight.org/%s/annotate" (see
course_materials/.../kwp_extraction/dbpedia/concept_tagging.py) to use the
shared DBPEDIA_SPOTLIGHT_URL or host variable so all services (including
AnnotationService) resolve via the same override/config.

Comment thread webapp/Dockerfile
# syntax=docker/dockerfile:1.5
FROM node:22.1-slim as build
# syntax=docker/dockerfile:1.15
FROM node:24.0-slim as build

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Retrieve digests for both Docker Hub images/tags
set -euo pipefail

fetch_digest () {
  local repo="$1"
  local tag="$2"
  local token
  token="$(curl -fsSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" | jq -r '.token')"
  echo "Digest for ${repo}:${tag}"
  curl -fsSI \
    -H "Authorization: Bearer ${token}" \
    -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
    "https://registry-1.docker.io/v2/${repo}/manifests/${tag}" \
    | awk 'BEGIN{IGNORECASE=1} /docker-content-digest/ {print $0}'
  echo
}

fetch_digest "library/node" "24.0-slim"
fetch_digest "nginxinc/nginx-unprivileged" "1.29.3-alpine"

Repository: ude-soco/CourseMapper-webserver

Length of output: 355


🏁 Script executed:

cat -n webapp/Dockerfile | head -25

Repository: ude-soco/CourseMapper-webserver

Length of output: 859


Pin both base images to immutable digests.

Lines 2 and 20 use mutable tags (node:24.0-slim, nginxinc/nginx-unprivileged:1.29.3-alpine), which makes rebuilds non-deterministic. Update to:

  • Line 2: FROM node:24.0-slim@sha256:083430e81f23ca4f309c6de17614d20706ddd544b2adc71fb9fdd86e2371360a as build
  • Line 20: FROM nginxinc/nginx-unprivileged:1.29.3-alpine@sha256:5aea7cc516b419e3526f47dd1531be31a56a046cfe44754d94f9383e13e2ee99
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/Dockerfile` at line 2, Replace the mutable base image tags in the
Dockerfile with the provided immutable digests: update the build stage FROM
reference (currently "node:24.0-slim" in the FROM line used by the build stage)
to the pinned digest
"node:24.0-slim@sha256:083430e81f23ca4f309c6de17614d20706ddd544b2adc71fb9fdd86e2371360a",
and update the final runtime FROM reference (currently
"nginxinc/nginx-unprivileged:1.29.3-alpine") to the pinned digest
"nginxinc/nginx-unprivileged:1.29.3-alpine@sha256:5aea7cc516b419e3526f47dd1531be31a56a046cfe44754d94f9383e13e2ee99"
so rebuilds are deterministic.

Comment on lines +515 to +522
// const response = await axios.get(url);
const response = await axios.get(url, {
headers: {
// Use your app name + version + contact (email or URL)
"User-Agent": "CourseMapper (coursemapper@example.com)"
},
timeout: 10000
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace placeholder contact in User-Agent and drop the dead comment.

Wikimedia's User-Agent policy requires a legitimate way to contact the operator; coursemapper@example.com is a reserved placeholder domain (RFC 2606) and is non-routable. Requests may be throttled or blocked once traffic grows, and there is no way for Wikimedia ops to reach you. Use a real maintainer/project email or the project URL, and remove the leftover // const response = await axios.get(url); line.

🛠️ Proposed fix
-    // const response = await axios.get(url);
-     const response = await axios.get(url, {
-      headers: {
-        // Use your app name + version + contact (email or URL)
-        "User-Agent": "CourseMapper (coursemapper@example.com)"
-      },
-      timeout: 10000
-    });
+    const response = await axios.get(url, {
+      headers: {
+        // App name + version + contact (email or project URL), per Wikimedia UA policy
+        "User-Agent": "CourseMapper/1.0 (+https://github.com/ude-soco/CourseMapper-webserver)",
+      },
+      timeout: 10000,
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// const response = await axios.get(url);
const response = await axios.get(url, {
headers: {
// Use your app name + version + contact (email or URL)
"User-Agent": "CourseMapper (coursemapper@example.com)"
},
timeout: 10000
});
const response = await axios.get(url, {
headers: {
// App name + version + contact (email or project URL), per Wikimedia UA policy
"User-Agent": "CourseMapper/1.0 (+https://github.com/ude-soco/CourseMapper-webserver)",
},
timeout: 10000,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webserver/src/controllers/knowledgeGraph.controller.js` around lines 515 -
522, Replace the dead commented axios call and update the User-Agent header used
in the axios.get request inside the code that performs the Wikimedia API fetch:
remove the leftover "// const response = await axios.get(url);" line and change
"User-Agent": "CourseMapper (coursemapper@example.com)" to a real contact string
(project URL or maintainer email) per Wikimedia policy (e.g., "CourseMapper/1.0
(+https://yourproject.example.com)" or include a real maintainer email) in the
axios.get call so Wikimedia can contact the operator.

rawaa123 and others added 8 commits April 21, 2026 16:56
Bumps nginxinc/nginx-unprivileged from 1.29.3-alpine to 1.31.0-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-version: 1.31.0-alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps nginxinc/nginx-unprivileged from 1.29.3-alpine to 1.31.0-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-version: 1.31.0-alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](docker/setup-qemu-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](docker/setup-buildx-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](docker/build-push-action@v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](docker/metadata-action@v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow or composite action Medium

Unpinned 3rd party Action 'Build container images' step
Uses Step
uses 'docker/setup-qemu-action' with ref 'v4', not a pinned commit hash

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow or composite action Medium

Unpinned 3rd party Action 'Build container images' step
Uses Step
uses 'docker/setup-buildx-action' with ref 'v4', not a pinned commit hash
- name: Login to container registry
if: ${{ inputs.push }}
uses: docker/login-action@v3
uses: docker/login-action@v4

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow or composite action Medium

Unpinned 3rd party Action 'Build container images' step
Uses Step
uses 'docker/login-action' with ref 'v4', not a pinned commit hash
- name: Get tagging metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow or composite action Medium

Unpinned 3rd party Action 'Build container images' step
Uses Step: meta
uses 'docker/metadata-action' with ref 'v6', not a pinned commit hash

- name: Build and push container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7

Check warning

Code scanning / CodeQL

Unpinned tag for a non-immutable Action in workflow or composite action Medium

Unpinned 3rd party Action 'Build container images' step
Uses Step
uses 'docker/build-push-action' with ref 'v7', not a pinned commit hash
dependabot Bot added 5 commits June 22, 2026 19:52
Bumps nginxinc/nginx-unprivileged from 1.31.0-alpine to 1.31.2-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-version: 1.31.2-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps nginxinc/nginx-unprivileged from 1.31.0-alpine to 1.31.2-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-version: 1.31.2-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps nginxinc/nginx-unprivileged from 1.31.2-alpine to 1.31.3-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-version: 1.31.3-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps nginxinc/nginx-unprivileged from 1.31.2-alpine to 1.31.3-alpine.

---
updated-dependencies:
- dependency-name: nginxinc/nginx-unprivileged
  dependency-version: 1.31.3-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants