-
Notifications
You must be signed in to change notification settings - Fork 111
Support nested files and folders for Skills #2719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 250 commits
f919ec6
1293cce
faea8ef
0978471
a688d9f
4c4cdbd
8c90262
392a4fc
8c60c3c
cb1c945
ee9c975
ef1e05a
ccfa570
01275b4
3bf3acd
10e94e1
491c2bf
ee00dde
f874883
c057ce9
0530c0d
7ea3737
3a4b72b
4ba1ffc
7ffd874
444f5fe
9f5dec3
6e640c5
7c6cfbf
e9582ed
99660be
7f1deb8
4faa15e
f8f7811
8beb630
1d4274e
dd26c41
c84445e
2a2827f
c29601e
2c9cc78
fae2bb7
201167c
16edb18
21af464
1580432
ab0282d
d5ce67a
47df460
64d399f
35701af
640d79b
158b085
7e67975
cf1383e
86ef481
62d8def
281c7ca
7687e50
b410f66
358bb93
071f32b
54de1f5
487a505
1d932b3
03987b2
9e8c78c
7074df5
744889e
21130e1
4649df8
c2c889b
7c355d2
21f2810
9540eed
5aaf1cd
d827cef
287b994
e96d775
4375254
ce7789e
8eb3986
21d8d64
b60b0fd
08088b5
37e13eb
ef424e2
bca6fc1
0aa7d7c
ebb09c0
25ec4e0
ebd659b
4379732
b75fbee
6fbd6b9
ce84448
2426479
620a43f
2b6abc4
e7152e4
c5429d4
7da634c
fc48bd4
3c706f2
1182452
6b369d8
4753c9b
4d3f611
84e4b90
cf620d3
3065541
c9a66c6
e4d9c94
97fdf7b
d0928e9
08ddef8
65c5c82
c69831d
8f7a155
a94a2b2
35e5ff6
c383c1d
93e5513
c66b6ec
61e9774
06dd980
2d0f520
8405c23
545b3ef
b56f746
5b867ec
bbb0b2d
2735c78
82c2bf7
5431287
e4abbfa
c8cb267
6cbe1e9
e87fd5c
0bfb16d
69a7433
dbfe03f
6ce533c
76bda40
2027327
2dc8f81
d7a91b3
d17ed3d
49e8629
bb2a66a
cd9a686
382e89a
c635cd1
8b5bce6
fe6fedc
b76aeaf
f51b590
8e1fa7e
b653e82
7e81757
7ed5e6d
cc08989
0fa612c
ae911bf
57578ce
d46d82e
8c24240
b3e4aa2
3166324
4ebc877
a39ee21
7dbfdaf
9d9ad43
6846de6
c37bec3
7b9e673
310ecb4
b695a58
425cbb2
af55bdb
8fc81c6
9ad4137
8add5ea
4215eaa
1d7ce79
5d2faa8
5ff2fdc
0c91c04
3720d6d
19a161f
1c06a46
907eb2d
ef1f1f2
7f40c20
5bfef51
3237fe5
f0f3401
1c7bcbe
6b4f09a
4590131
44c142c
3607676
0ddc49d
62576ff
bacc80c
e688fcb
3ff87a6
ceb4e98
e34817d
36522b0
fee5b8b
c58b74f
e038177
362b30a
b201830
3e53004
a9cbf7c
f6251e8
766ed10
5c7356a
d3f8226
b8bc05b
c27a7e8
a32efde
d0d227f
fd6fd6a
a140951
e920708
0ff5172
4609838
798e050
75f0633
8e5e90f
a201741
2feecf2
a8c893b
7656cb0
99986e7
d95b2b0
9973795
a86f14a
cf4c821
10e9ae3
fc007d9
9c767e6
92b5b01
bd73ee1
aa3c042
b2bba9f
f72940a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,31 @@ | ||
| import { OpenAPIHono } from '@hono/zod-openapi'; | ||
| import { OpenAPIHono, z } from '@hono/zod-openapi'; | ||
| import { | ||
| commonGetErrorResponses, | ||
| createApiError, | ||
| createSkill, | ||
| createSkillFileById, | ||
| deleteSkill, | ||
| getSkillById, | ||
| deleteSkillFileById, | ||
| getSkillByIdWithFiles, | ||
| getSkillFileById, | ||
| listSkills, | ||
| PaginationQueryParamsSchema, | ||
| ResourceIdSchema, | ||
| SKILL_ENTRY_FILE_PATH, | ||
| SkillApiInsertSchema, | ||
| SkillApiUpdateSchema, | ||
| SkillFileApiInsertSchema, | ||
| SkillFileApiUpdateSchema, | ||
| SkillFileResponse, | ||
| SkillListResponse, | ||
| SkillResponse, | ||
| SkillWithFilesResponse, | ||
| TenantProjectIdParamsSchema, | ||
| TenantProjectParamsSchema, | ||
| updateSkill, | ||
| updateSkillFileById, | ||
| } from '@inkeep/agents-core'; | ||
| import { createProtectedRoute } from '@inkeep/agents-core/middleware'; | ||
| import { HTTPException } from 'hono/http-exception'; | ||
| import { requireProjectPermission } from '../../../middleware/projectAccess'; | ||
| import type { ManageAppVariables } from '../../../types'; | ||
| import { | ||
|
|
@@ -25,6 +35,9 @@ import { | |
| import { speakeasyOffsetLimitPagination } from '../../../utils/speakeasy'; | ||
|
|
||
| const app = new OpenAPIHono<{ Variables: ManageAppVariables }>(); | ||
| const TenantProjectSkillFileParamsSchema = TenantProjectIdParamsSchema.extend({ | ||
| fileId: ResourceIdSchema, | ||
| }); | ||
|
|
||
| app.openapi( | ||
| createProtectedRoute({ | ||
|
|
@@ -65,6 +78,116 @@ app.openapi( | |
| } | ||
| ); | ||
|
|
||
| app.openapi( | ||
| createProtectedRoute({ | ||
| method: 'post', | ||
| path: '/{id}/files', | ||
| summary: 'Create Skill File', | ||
| operationId: 'create-skill-file', | ||
| tags: ['Skills'], | ||
| permission: requireProjectPermission('edit'), | ||
| request: { | ||
| params: TenantProjectIdParamsSchema, | ||
| body: { | ||
| content: { | ||
| 'application/json': { | ||
| schema: SkillFileApiInsertSchema, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| responses: { | ||
| 201: { | ||
| description: 'Skill file created successfully', | ||
| content: { | ||
| 'application/json': { | ||
| schema: SkillFileResponse, | ||
| }, | ||
| }, | ||
| }, | ||
| ...commonGetErrorResponses, | ||
| }, | ||
| }), | ||
| async (c) => { | ||
| const db = c.get('db'); | ||
| const { tenantId, projectId, id } = c.req.valid('param'); | ||
| const body = c.req.valid('json'); | ||
|
|
||
| try { | ||
| const file = await createSkillFileById(db)({ | ||
| scopes: { tenantId, projectId }, | ||
| skillId: id, | ||
| data: body, | ||
| }); | ||
|
|
||
| if (!file) { | ||
| throw createApiError({ | ||
| code: 'not_found', | ||
| message: 'Skill not found', | ||
| }); | ||
| } | ||
|
|
||
| return c.json({ data: file }, 201); | ||
| } catch (error) { | ||
| if (error instanceof HTTPException) { | ||
| throw error; | ||
| } | ||
|
|
||
| if (error instanceof Error) { | ||
| throw createApiError({ | ||
| code: error.message.includes('already exists') ? 'conflict' : 'unprocessable_entity', | ||
| message: error.message, | ||
| }); | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| app.openapi( | ||
| createProtectedRoute({ | ||
| method: 'get', | ||
| path: '/{id}/files/{fileId}', | ||
| summary: 'Get Skill File', | ||
| operationId: 'get-skill-file', | ||
| tags: ['Skills'], | ||
| permission: requireProjectPermission('view'), | ||
| request: { | ||
| params: TenantProjectSkillFileParamsSchema, | ||
| }, | ||
| responses: { | ||
| 200: { | ||
| description: 'Skill file found', | ||
| content: { | ||
| 'application/json': { | ||
| schema: SkillFileResponse, | ||
| }, | ||
| }, | ||
| }, | ||
| ...commonGetErrorResponses, | ||
| }, | ||
| }), | ||
| async (c) => { | ||
| const db = c.get('db'); | ||
| const { tenantId, projectId, id, fileId } = c.req.valid('param'); | ||
| const file = await getSkillFileById(db)({ | ||
| scopes: { tenantId, projectId }, | ||
| skillId: id, | ||
| fileId, | ||
| }); | ||
|
|
||
| if (!file) { | ||
| throw createApiError({ | ||
| code: 'not_found', | ||
| message: 'Skill file not found', | ||
| }); | ||
| } | ||
|
|
||
| return c.json({ data: file }); | ||
| } | ||
| ); | ||
|
|
||
| app.openapi( | ||
| createProtectedRoute({ | ||
| method: 'get', | ||
|
|
@@ -81,7 +204,7 @@ app.openapi( | |
| description: 'Skill found', | ||
| content: { | ||
| 'application/json': { | ||
| schema: SkillResponse, | ||
| schema: SkillWithFilesResponse, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -91,7 +214,7 @@ app.openapi( | |
| async (c) => { | ||
| const db = c.get('db'); | ||
| const { tenantId, projectId, id } = c.req.valid('param'); | ||
| const skill = await getSkillById(db)({ | ||
| const skill = await getSkillByIdWithFiles(db)({ | ||
| scopes: { tenantId, projectId }, | ||
| skillId: id, | ||
| }); | ||
|
|
@@ -130,7 +253,7 @@ app.openapi( | |
| description: 'Skill created successfully', | ||
| content: { | ||
| 'application/json': { | ||
| schema: SkillResponse, | ||
| schema: SkillWithFilesResponse, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -172,7 +295,7 @@ const updateSkillRouteConfig = { | |
| description: 'Skill updated successfully', | ||
| content: { | ||
| 'application/json': { | ||
| schema: SkillResponse, | ||
| schema: SkillWithFilesResponse, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -185,10 +308,14 @@ const updateSkillHandler: ManageRouteHandler<typeof updateSkillRouteConfig> = as | |
| const { tenantId, projectId, id } = c.req.valid('param'); | ||
| const body = c.req.valid('json'); | ||
|
|
||
| const data = body.files.some((file) => file.filePath === SKILL_ENTRY_FILE_PATH) | ||
| ? body | ||
| : { files: [] }; | ||
|
Comment on lines
+311
to
+313
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the update payload doesn't include |
||
|
|
||
| const skill = await updateSkill(db)({ | ||
| scopes: { tenantId, projectId }, | ||
| skillId: id, | ||
| data: body, | ||
| data, | ||
| }); | ||
|
|
||
| if (!skill) { | ||
|
|
@@ -205,6 +332,81 @@ openapiRegisterPutPatchRoutesForLegacy(app, updateSkillRouteConfig, updateSkillH | |
| operationId: 'update-skill', | ||
| }); | ||
|
|
||
| app.openapi( | ||
| createProtectedRoute({ | ||
| method: 'patch', | ||
| path: '/{id}/files/{fileId}', | ||
| summary: 'Update Skill File', | ||
| operationId: 'update-skill-file', | ||
| tags: ['Skills'], | ||
| permission: requireProjectPermission('edit'), | ||
| request: { | ||
| params: TenantProjectSkillFileParamsSchema, | ||
| body: { | ||
| content: { | ||
| 'application/json': { | ||
| schema: SkillFileApiUpdateSchema, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| responses: { | ||
| 200: { | ||
| description: 'Skill file updated successfully', | ||
| content: { | ||
| 'application/json': { | ||
| schema: SkillFileResponse, | ||
| }, | ||
| }, | ||
| }, | ||
| ...commonGetErrorResponses, | ||
| }, | ||
| }), | ||
| async (c) => { | ||
| const db = c.get('db'); | ||
| const { tenantId, projectId, id, fileId } = c.req.valid('param'); | ||
| const body = c.req.valid('json'); | ||
|
|
||
| try { | ||
| const file = await updateSkillFileById(db)({ | ||
| scopes: { tenantId, projectId }, | ||
| skillId: id, | ||
| fileId, | ||
| ...body, | ||
| }); | ||
|
|
||
| if (!file) { | ||
| throw createApiError({ | ||
| code: 'not_found', | ||
| message: 'Skill file not found', | ||
| }); | ||
| } | ||
|
|
||
| return c.json({ data: file }); | ||
| } catch (error) { | ||
| if (error instanceof HTTPException) { | ||
| throw error; | ||
| } | ||
|
|
||
| if (error instanceof z.ZodError) { | ||
| throw createApiError({ | ||
| code: 'unprocessable_entity', | ||
| message: 'Invalid skill file payload', | ||
| }); | ||
| } | ||
|
|
||
| if (error instanceof Error) { | ||
| throw createApiError({ | ||
| code: 'unprocessable_entity', | ||
| message: error.message, | ||
| }); | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| app.openapi( | ||
| createProtectedRoute({ | ||
| method: 'delete', | ||
|
|
@@ -243,4 +445,58 @@ app.openapi( | |
| } | ||
| ); | ||
|
|
||
| app.openapi( | ||
| createProtectedRoute({ | ||
| method: 'delete', | ||
| path: '/{id}/files/{fileId}', | ||
| summary: 'Delete Skill File', | ||
| operationId: 'delete-skill-file', | ||
| tags: ['Skills'], | ||
| permission: requireProjectPermission('edit'), | ||
| request: { | ||
| params: TenantProjectSkillFileParamsSchema, | ||
| }, | ||
| responses: { | ||
| 204: { | ||
| description: 'Skill file deleted successfully', | ||
| }, | ||
| ...commonGetErrorResponses, | ||
| }, | ||
| }), | ||
| async (c) => { | ||
| const db = c.get('db'); | ||
| const { tenantId, projectId, id, fileId } = c.req.valid('param'); | ||
|
|
||
| try { | ||
| const removed = await deleteSkillFileById(db)({ | ||
| scopes: { tenantId, projectId }, | ||
| skillId: id, | ||
| fileId, | ||
| }); | ||
|
|
||
| if (!removed) { | ||
| throw createApiError({ | ||
| code: 'not_found', | ||
| message: 'Skill file not found', | ||
| }); | ||
| } | ||
|
|
||
| return c.body(null, 204); | ||
| } catch (error) { | ||
| if (error instanceof HTTPException) { | ||
| throw error; | ||
| } | ||
|
|
||
| if (error instanceof Error) { | ||
| throw createApiError({ | ||
| code: 'unprocessable_entity', | ||
| message: error.message, | ||
| }); | ||
| } | ||
|
|
||
| throw error; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| export default app; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaking internal error messages in development mode is useful for debugging but make sure
error.messagecan't contain sensitive data (e.g. SQL query fragments, connection strings). Drizzle errors can sometimes include query text.