Skip to content

Commit 9996595

Browse files
authored
Merge pull request #77 from akvo/feature/76-update-registered-app-implement-default-prompt
Feature/76 update registered app implement default prompt
2 parents 73bcdb3 + 4c996e0 commit 9996595

File tree

7 files changed

+277
-19
lines changed

7 files changed

+277
-19
lines changed

backend/app/api/api_v1/apps.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
AppRotateResponse,
2222
ErrorResponse,
2323
DocumentUploadItem,
24+
AppUpdateRequest,
25+
AppUpdateResponse
2426
)
2527
from mcp_clients.kb_mcp_endpoint_service import KnowledgeBaseMCPEndpointService
2628

@@ -239,3 +241,56 @@ async def get_documents(
239241
kb_id=current_app.knowledge_base_id,
240242
)
241243
return res
244+
245+
246+
@router.patch(
247+
"",
248+
response_model=AppUpdateResponse,
249+
responses={
250+
400: {"model": ErrorResponse, "description": "Validation error"},
251+
401: {"model": ErrorResponse, "description": "Unauthorized - Invalid or missing token"},
252+
403: {"model": ErrorResponse, "description": "Forbidden - Inactive app"},
253+
404: {"model": ErrorResponse, "description": "App not found"},
254+
},
255+
)
256+
def update_app(
257+
*,
258+
db: Session = Depends(get_db),
259+
current_app: App = Depends(get_current_app),
260+
update_data: AppUpdateRequest,
261+
) -> Any:
262+
"""
263+
Partially update app metadata.
264+
265+
Only provided fields will be updated. Access token and client_id
266+
will remain unchanged.
267+
268+
Requires Bearer token authentication.
269+
"""
270+
if not AppService.is_app_active(current_app):
271+
raise HTTPException(
272+
status_code=status.HTTP_403_FORBIDDEN,
273+
detail="App is not active",
274+
)
275+
276+
try:
277+
updated_app = AppService.update_app(
278+
db=db,
279+
app=current_app,
280+
update_data=update_data,
281+
)
282+
283+
return AppUpdateResponse(
284+
app_id=updated_app.app_id,
285+
app_name=updated_app.app_name,
286+
domain=updated_app.domain,
287+
default_chat_prompt=updated_app.default_chat_prompt,
288+
chat_callback=updated_app.chat_callback_url,
289+
upload_callback=updated_app.upload_callback_url,
290+
callback_token=updated_app.callback_token,
291+
updated_at=updated_app.updated_at,
292+
)
293+
except ValueError as e:
294+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
295+
except Exception as e:
296+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update app: {str(e)}")

backend/app/api/api_v1/jobs.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,35 @@ async def create_job(
4141
Form(
4242
...,
4343
description=(
44-
"JSON string defining the job details.\n\n"
45-
"Example:\n"
46-
"{\n"
47-
' "job": "chat",\n'
48-
' "prompt": "Explain AI simply.",\n'
49-
' "chats": [\n'
50-
' {"role": "user", "content": "What is AI?"},\n'
51-
' {"role": "assistant", "content": "AI means Artificial Intelligence."}\n'
52-
' ],\n'
53-
' "callback_params": {"reply_to": "wa:+1234"},\n'
54-
' "trace_id": "trace_abc_123"\n'
55-
"}"
44+
"""
45+
JSON string defining the job details.
46+
Example chat job:
47+
"{
48+
"job": "chat",
49+
"prompt": "Explain AI simply.",
50+
"chats": [
51+
{"role": "user", "content": "What is AI?"},
52+
{"role": "assistant", "content": "AI means Artificial Intelligence."}
53+
],
54+
"callback_params": {"reply_to": "wa:+1234"},
55+
"trace_id": "trace_abc_123"
56+
}"
57+
58+
---
59+
60+
Example upload job:
61+
"{
62+
"job": "upload",
63+
"metadata": {
64+
"kb_id": 1,
65+
"title": "Chlorination SOP",
66+
"tags": ["chlorine","ops"]
67+
},
68+
"callback_params": {
69+
"ui_upload_id": "up_456"
70+
}
71+
}"
72+
"""
5673
),
5774
),
5875
],
@@ -93,6 +110,7 @@ async def create_job(
93110
job_id=job_record.id,
94111
data=data,
95112
callback_url=current_app.chat_callback_url,
113+
app_default_prompt=current_app.default_chat_prompt,
96114
knowledge_base_ids=knowledge_base_ids,
97115
)
98116
elif job_type == "upload":

backend/app/schemas/app.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,36 @@ class Config:
3232
from_attributes = True
3333

3434

35+
class AppUpdateRequest(BaseModel):
36+
app_name: Optional[str] = None
37+
domain: Optional[str] = None
38+
default_chat_prompt: Optional[str] = None
39+
chat_callback: Optional[str] = None
40+
upload_callback: Optional[str] = None
41+
callback_token: Optional[str] = None
42+
43+
@field_validator("chat_callback", "upload_callback")
44+
@classmethod
45+
def validate_https_url(cls, v: Optional[str]) -> Optional[str]:
46+
if v is not None and not v.startswith("https://"):
47+
raise ValueError("Callback URLs must use HTTPS")
48+
return v
49+
50+
51+
class AppUpdateResponse(BaseModel):
52+
app_id: str
53+
app_name: str
54+
domain: str
55+
default_chat_prompt: Optional[str] = None
56+
chat_callback: str
57+
upload_callback: str
58+
callback_token: Optional[str] = None
59+
updated_at: datetime
60+
61+
class Config:
62+
from_attributes = True
63+
64+
3565
class AppMeResponse(BaseModel):
3666
app_id: str
3767
app_name: str

backend/app/services/app_service.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from sqlalchemy.orm import Session
44

55
from app.models.app import App, AppStatus
6-
from app.schemas.app import AppRegisterRequest
6+
from app.schemas.app import AppRegisterRequest, AppUpdateRequest
77

88
# Default scopes for new apps
99
DEFAULT_SCOPES = ["jobs.write", "kb.read", "kb.write", "apps.read"]
@@ -64,6 +64,33 @@ def create_app(
6464

6565
return app, access_token
6666

67+
@staticmethod
68+
def update_app(
69+
db: Session,
70+
app: App,
71+
update_data: AppUpdateRequest,
72+
) -> App:
73+
"""
74+
Partially update app fields without resetting identifiers or tokens."""
75+
updated = False
76+
data = update_data.dict(exclude_unset=True)
77+
78+
for field, value in data.items():
79+
if value is not None:
80+
if field == "chat_callback":
81+
app.chat_callback_url = value
82+
elif field == "upload_callback":
83+
app.upload_callback_url = value
84+
elif hasattr(app, field):
85+
setattr(app, field, value)
86+
updated = True
87+
88+
if updated:
89+
db.add(app)
90+
db.commit()
91+
db.refresh(app)
92+
return app
93+
6794
@staticmethod
6895
def get_app_by_access_token(db: Session, access_token: str) -> Optional[App]:
6996
"""Get app by access token."""

backend/app/services/chat_job_service.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ async def execute_chat_job(
1616
job_id: str,
1717
data: dict,
1818
callback_url: str,
19+
app_default_prompt: str,
1920
knowledge_base_ids: List[int] = []
2021
):
2122
"""Background job executor for chat jobs (non-streaming)."""
@@ -26,12 +27,19 @@ async def execute_chat_job(
2627
try:
2728
JobService.update_status_to_running(db, job_id)
2829

30+
# prompt from app logic
31+
app_default_prompt = app_default_prompt or None
32+
job_payload_prompt = data.get("prompt") or None
33+
app_final_prompt = (
34+
job_payload_prompt if job_payload_prompt else app_default_prompt
35+
) or ""
36+
# eol prompt from app logic
37+
2938
prompt_service = PromptService(db=db)
3039
settings_service = SystemSettingsService(db=db)
3140
top_k = settings_service.get_top_k()
3241

3342
chats = data.get("chats", [])
34-
prompt = data.get("prompt") or None
3543
callback_url = data.get("callback_url") or callback_url
3644

3745
chat_history = []
@@ -44,14 +52,18 @@ async def execute_chat_job(
4452
query = chat_history[-1].get("content") if chat_history else ""
4553
chat_history = chat_history[:-1] if chat_history else []
4654

55+
# default rag prompt
4756
contextualize_prompt = prompt_service.get_full_contextualize_prompt()
4857
qa_prompt = prompt_service.get_full_qa_strict_prompt()
4958

59+
# combined prompt
60+
final_prompt = qa_prompt + "\n\n" + app_final_prompt
61+
5062
state = {
5163
"query": query,
5264
"chat_history": chat_history,
5365
"contextualize_prompt_str": contextualize_prompt,
54-
"qa_prompt_str": prompt or qa_prompt,
66+
"qa_prompt_str": final_prompt,
5567
"scope": {
5668
"knowledge_base_ids": knowledge_base_ids,
5769
"top_k": top_k,

backend/app/tasks/chat_task.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def execute_chat_job_task(
99
job_id: str,
1010
data: dict,
1111
callback_url: str,
12+
app_default_prompt: str,
1213
knowledge_base_ids: list[int] = [],
1314
):
1415
"""
@@ -22,6 +23,7 @@ def execute_chat_job_task(
2223
job_id=job_id,
2324
data=data,
2425
callback_url=callback_url,
26+
app_default_prompt=app_default_prompt,
2527
knowledge_base_ids=knowledge_base_ids
2628
)
2729
)

0 commit comments

Comments
 (0)