Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions api/controllers/console/explore/recommended_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,28 @@ class RecommendedAppListResponse(ResponseModel):
categories: list[str]


class LearnDifyAppListResponse(ResponseModel):
recommended_apps: list[RecommendedAppResponse]


register_schema_models(
console_ns,
RecommendedAppsQuery,
RecommendedAppInfoResponse,
RecommendedAppResponse,
RecommendedAppListResponse,
LearnDifyAppListResponse,
)


def _resolve_language(language: str | None) -> str:
if language and language in languages:
return language
if current_user and current_user.interface_language:
return current_user.interface_language
return languages[0]


@console_ns.route("/explore/apps")
class RecommendedAppListApi(Resource):
@console_ns.doc(params=query_params_from_model(RecommendedAppsQuery))
Expand All @@ -82,20 +95,30 @@ class RecommendedAppListApi(Resource):
def get(self):
# language args
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
language = args.language
if language and language in languages:
language_prefix = language
elif current_user and current_user.interface_language:
language_prefix = current_user.interface_language
else:
language_prefix = languages[0]
language_prefix = _resolve_language(args.language)

return RecommendedAppListResponse.model_validate(
RecommendedAppService.get_recommended_apps_and_categories(language_prefix),
from_attributes=True,
).model_dump(mode="json")


@console_ns.route("/explore/apps/learn-dify")
class LearnDifyAppListApi(Resource):
@console_ns.doc(params=query_params_from_model(RecommendedAppsQuery))
@console_ns.response(200, "Success", console_ns.models[LearnDifyAppListResponse.__name__])
@login_required
@account_initialization_required
def get(self):
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
language_prefix = _resolve_language(args.language)

return LearnDifyAppListResponse.model_validate(
RecommendedAppService.get_learn_dify_apps(language_prefix),
from_attributes=True,
).model_dump(mode="json")


@console_ns.route("/explore/apps/<uuid:app_id>")
class RecommendedAppApi(Resource):
@login_required
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""add learn dify flag to recommended apps

Revision ID: f5e8a9c0d2b3
Revises: a4f2d8c9b731
Create Date: 2026-05-18 15:00:00.000000

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "f5e8a9c0d2b3"
down_revision = "a4f2d8c9b731"
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
batch_op.add_column(sa.Column("is_learn_dify", sa.Boolean(), server_default=sa.text("false"), nullable=False))


def downgrade():
with op.batch_alter_table("recommended_apps", schema=None) as batch_op:
batch_op.drop_column("is_learn_dify")
3 changes: 3 additions & 0 deletions api/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,9 @@ class RecommendedApp(TypeBase):
custom_disclaimer: Mapped[str] = mapped_column(LongText, default="")
position: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
is_listed: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=True)
is_learn_dify: Mapped[bool] = mapped_column(
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
)
install_count: Mapped[int] = mapped_column(sa.Integer, nullable=False, default=0)
language: Mapped[str] = mapped_column(
String(255),
Expand Down
21 changes: 21 additions & 0 deletions api/openapi/markdown/console-swagger.md
Original file line number Diff line number Diff line change
Expand Up @@ -5400,6 +5400,21 @@ Delete an API key for a dataset
| ---- | ----------- | ------ |
| 200 | Success | [RecommendedAppListResponse](#recommendedapplistresponse) |

### /explore/apps/learn-dify

#### GET
##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| language | query | Language code for recommended app localization | No | string |

##### Responses

| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | [LearnDifyAppListResponse](#learndifyapplistresponse) |

### /explore/apps/{app_id}

#### GET
Expand Down Expand Up @@ -12234,6 +12249,12 @@ Enum class for large language model mode.
| ---- | ---- | ----------- | -------- |
| LLMMode | string | Enum class for large language model mode. | |

#### LearnDifyAppListResponse

| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| recommended_apps | [ [RecommendedAppResponse](#recommendedappresponse) ] | | Yes |

#### LegacyEndpointUpdatePayload

| Name | Type | Description | Required |
Expand Down
2 changes: 1 addition & 1 deletion api/services/app_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def get_paginate_apps(self, user_id: str, tenant_id: str, params: AppListParams)
return None

app_models = db.paginate(
sa.select(App).where(*filters).order_by(App.created_at.desc()),
sa.select(App).where(*filters).order_by(App.updated_at.desc()),
page=params.page,
per_page=params.limit,
error_out=False,
Expand Down
48 changes: 41 additions & 7 deletions api/services/recommend_app/database/database_retrieval.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, TypedDict
from typing import Any, NotRequired, TypedDict

from sqlalchemy import select

Expand All @@ -22,6 +22,7 @@ class RecommendedAppItemDict(TypedDict):
categories: list[str]
position: int
is_listed: bool
can_trial: NotRequired[bool]


class RecommendedAppsResultDict(TypedDict):
Expand Down Expand Up @@ -61,14 +62,47 @@ def fetch_recommended_apps_from_db(cls, language: str) -> RecommendedAppsResultD
:param language: language
:return:
"""
recommended_apps = db.session.scalars(
select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.language == language)
).all()
recommended_apps = cls._fetch_listed_recommended_apps(language)

if len(recommended_apps) == 0:
recommended_apps = db.session.scalars(
select(RecommendedApp).where(RecommendedApp.is_listed == True, RecommendedApp.language == languages[0])
).all()
recommended_apps = cls._fetch_listed_recommended_apps(languages[0])

return cls._format_recommended_apps(recommended_apps, language)

@classmethod
def fetch_learn_dify_apps_from_db(cls, language: str) -> RecommendedAppsResultDict:
"""
Fetch listed recommended apps explicitly marked for the Learn Dify section.
:param language: language
:return:
"""
recommended_apps = cls._fetch_listed_recommended_apps(language, is_learn_dify=True)

if len(recommended_apps) == 0 and language != languages[0]:
recommended_apps = cls._fetch_listed_recommended_apps(languages[0], is_learn_dify=True)

return cls._format_recommended_apps(recommended_apps, language)

@classmethod
def _fetch_listed_recommended_apps(
cls, language: str, *, is_learn_dify: bool | None = None
) -> list[RecommendedApp]:
filters = [RecommendedApp.is_listed.is_(True), RecommendedApp.language == language]
if is_learn_dify is not None:
filters.append(RecommendedApp.is_learn_dify.is_(is_learn_dify))

return db.session.scalars(select(RecommendedApp).where(*filters)).all()

@classmethod
def _format_recommended_apps(
cls, recommended_apps: list[RecommendedApp], language: str
) -> RecommendedAppsResultDict:
"""
Serialize DB recommended app rows into the Explore list response shape.
:param recommended_apps: recommended app rows
:param language: language used for category ordering
:return:
"""

categories = set()
recommended_apps_result: list[RecommendedAppItemDict] = []
Expand Down
33 changes: 23 additions & 10 deletions api/services/recommended_app_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from extensions.ext_database import db
from models.model import AccountTrialAppRecord, TrialApp
from services.feature_service import FeatureService
from services.recommend_app.database.database_retrieval import DatabaseRecommendAppRetrieval
from services.recommend_app.recommend_app_factory import RecommendAppRetrievalFactory


Expand All @@ -31,13 +32,24 @@ def get_recommended_apps_and_categories(cls, language: str):
apps = result["recommended_apps"]
for app in apps:
app_id = app["app_id"]
trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
if trial_app_model:
app["can_trial"] = True
else:
app["can_trial"] = False
app["can_trial"] = cls._can_trial_app(app_id)
return result

@classmethod
def get_learn_dify_apps(cls, language: str) -> dict[str, Any]:
"""
Get database-backed recommended apps marked as Learn Dify.
:param language: language
:return:
"""
result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db(language)

if FeatureService.get_system_features().enable_trial_app:
for app in result["recommended_apps"]:
app["can_trial"] = cls._can_trial_app(app["app_id"])

return {"recommended_apps": result["recommended_apps"]}

@classmethod
def get_recommend_app_detail(cls, app_id: str) -> dict[str, Any] | None:
"""
Expand All @@ -52,11 +64,7 @@ def get_recommend_app_detail(cls, app_id: str) -> dict[str, Any] | None:
return None
if FeatureService.get_system_features().enable_trial_app:
app_id = result["id"]
trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
if trial_app_model:
result["can_trial"] = True
else:
result["can_trial"] = False
result["can_trial"] = cls._can_trial_app(app_id)
return result

@classmethod
Expand All @@ -77,3 +85,8 @@ def add_trial_app_record(cls, app_id: str, account_id: str):
else:
db.session.add(AccountTrialAppRecord(app_id=app_id, count=1, account_id=account_id))
db.session.commit()

@staticmethod
def _can_trial_app(app_id: str) -> bool:
trial_app_model = db.session.scalar(select(TrialApp).where(TrialApp.app_id == app_id).limit(1))
return trial_app_model is not None
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def _create_recommended_app(
categories: list[str] | None = None,
language: str = "en-US",
is_listed: bool = True,
is_learn_dify: bool = False,
position: int = 1,
) -> RecommendedApp:
rec = RecommendedApp(
Expand All @@ -61,6 +62,7 @@ def _create_recommended_app(
categories=[category] if categories is None else categories,
language=language,
is_listed=is_listed,
is_learn_dify=is_learn_dify,
position=position,
)
rec.id = str(uuid4())
Expand Down Expand Up @@ -202,6 +204,65 @@ def test_skips_apps_without_site(self, flask_app_with_containers, db_session_wit
app_ids = {r["app_id"] for r in result["recommended_apps"]}
assert app1.id not in app_ids

def test_fetch_learn_dify_apps_uses_flag_not_categories(
self,
flask_app_with_containers,
db_session_with_containers: Session,
):
tenant_id = str(uuid4())
learn_dify_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
_create_site(db_session_with_containers, app_id=learn_dify_app.id)
_create_recommended_app(
db_session_with_containers,
app_id=learn_dify_app.id,
category="workflow",
categories=["Workflow"],
is_learn_dify=True,
)

category_only_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
_create_site(db_session_with_containers, app_id=category_only_app.id)
_create_recommended_app(
db_session_with_containers,
app_id=category_only_app.id,
category="Learn Dify",
categories=["Learn Dify"],
is_learn_dify=False,
)

db_session_with_containers.expire_all()

result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db("en-US")

app_ids = {r["app_id"] for r in result["recommended_apps"]}
assert learn_dify_app.id in app_ids
assert category_only_app.id not in app_ids
recommended_app = next(item for item in result["recommended_apps"] if item["app_id"] == learn_dify_app.id)
assert recommended_app["categories"] == ["Workflow"]

def test_fetch_learn_dify_apps_falls_back_to_default_language(
self,
flask_app_with_containers,
db_session_with_containers: Session,
):
tenant_id = str(uuid4())
learn_dify_app = _create_app(db_session_with_containers, tenant_id=tenant_id)
_create_site(db_session_with_containers, app_id=learn_dify_app.id)
_create_recommended_app(
db_session_with_containers,
app_id=learn_dify_app.id,
categories=["Workflow"],
is_learn_dify=True,
language="en-US",
)

db_session_with_containers.expire_all()

result = DatabaseRecommendAppRetrieval.fetch_learn_dify_apps_from_db("fr-FR")

app_ids = {r["app_id"] for r in result["recommended_apps"]}
assert learn_dify_app.id in app_ids


class TestFetchRecommendedAppDetailFromDb:
def test_returns_none_when_not_listed(self, flask_app_with_containers, db_session_with_containers: Session):
Expand Down
Loading
Loading