Language Server API for AI (#9679)

close #9656

Changelog:
- add: `ai/completion_v2` method
- add: `Visualization.AI.print` method for converting the expression to text format
- update: The default system prompt was updated to tell AI to use the `Visualization.AI.print` method for printing.

# Important Notes
The project [New_Project_1.zip](https://github.com/enso-org/enso/files/15152993/New_Project_1.zip) contains the following main file:
```py
from Standard.Base import all
from Standard.Table import all
from Standard.Database import all
from Standard.AWS import all
import Standard.Visualization
import Standard.Visualization.Warnings
from Standard.Base.Errors.Common import Dry_Run_Operation

type Student
Value id region

main =
operator70395 = 226
operator47321 = 'east'
operator76980 = Student.Value operator70395 operator47321
operator31302 = operator47321.words True
operator91574 = 1
operator34358 = operator47321.take (Index_Sub_Range.By_Index [0, operator91574])



#### METADATA ####
[[{"index":{"value":0},"size":{"value":4}},"4cf8de7f-2014-4dfd-9ceb-0164fe26c8bf"],[{"index":{"value":0},"size":{"value":29}},"389fe7a5-e59b-440a-8801-e0bd86716094"],[{"index":{"value":0},"size":{"value":558}},"a8a81ce3-199e-479e-98dd-5f919425842d"],[{"index":{"value":5},"size":{"value":8}},"c6f0de99-3bd3-459d-a69b-5fc774dd1e3f"],[{"index":{"value":5},"size":{"value":13}},"05827411-7354-478b-99d8-8370dadd560b"],[{"index":{"value":13},"size":{"value":1}},"41de94cc-ccd2-4ccf-b9db-cf686443efa0"],[{"index":{"value":14},"size":{"value":4}},"8abd24ea-b1cb-4c10-ad16-5a190f7ed0c7"],[{"index":{"value":19},"size":{"value":6}},"5c57a62d-ed82-4195-8b78-2ed660556dd0"],[{"index":{"value":26},"size":{"value":3}},"23544334-1117-4dbc-ac7f-bb3f84575264"],[{"index":{"value":30},"size":{"value":4}},"6058ba89-dc1b-45e1-b8f3-d1a4d27975f7"],[{"index":{"value":30},"size":{"value":30}},"bdd4b4cc-9c1e-4554-b0fb-3f73dcd5a590"],[{"index":{"value":35},"size":{"value":8}},"7e7bb9b4-8e9b-4700-9dcd-83c353800da5"],[{"index":{"value":35},"size":{"value":14}},"524bafb6-d3ca-4ee5-b8de-84b5c47ca87b"],[{"index":{"value":43},"size":{"value":1}},"b68b3929-8b63-49ad-977c-b04e058f8ee4"],[{"index":{"value":44},"size":{"value":5}},"7173f374-5f53-462b-ab1b-f7b1845d729b"],[{"index":{"value":50},"size":{"value":6}},"911d381e-1bdb-4e9c-826d-a00495c8fd23"],[{"index":{"value":57},"size":{"value":3}},"2f8fa1ee-9d13-4a73-99b2-bc77d1656387"],[{"index":{"value":61},"size":{"value":4}},"11abd658-4888-476c-afcd-592edaaa53c1"],[{"index":{"value":61},"size":{"value":33}},"9f6ae949-410b-48e6-bb75-84d4ba8a1989"],[{"index":{"value":66},"size":{"value":8}},"f4726e40-dd5b-49a0-aa2a-6cea6df88d5d"],[{"index":{"value":66},"size":{"value":17}},"539b4dcf-4487-49b6-9c90-6ae58d51c2db"],[{"index":{"value":74},"size":{"value":1}},"a34a41a0-5dcf-4c47-a36c-a4f427c87c03"],[{"index":{"value":75},"size":{"value":8}},"8fdf4ee7-2e74-4cac-b9b9-4230183f91c5"],[{"index":{"value":84},"size":{"value":6}},"7ff7efdc-fed6-4cb6-bed6-392d110ff991"],[{"index":{"value":91},"size":{"value":3}},"eacff407-dc6d-4f17-a28b-329f7a0582e1"],[{"index":{"value":95},"size":{"value":4}},"f2feac9f-ea02-4070-879a-f1d493473993"],[{"index":{"value":95},"size":{"value":28}},"f9b37b67-fb54-4e47-8281-e922edb4c5d3"],[{"index":{"value":100},"size":{"value":8}},"6cf398f8-d0c9-42fa-a6e6-e740de11562f"],[{"index":{"value":100},"size":{"value":12}},"28e4d0e3-7da4-4ee2-94c7-d9a7c692f8de"],[{"index":{"value":108},"size":{"value":1}},"955e8788-816c-4eb7-b5ec-020b4863536a"],[{"index":{"value":109},"size":{"value":3}},"bdc65807-0438-4296-8b1f-1fab3a885c17"],[{"index":{"value":113},"size":{"value":6}},"fe3dd265-1855-4d66-8a2d-51153b21e699"],[{"index":{"value":120},"size":{"value":3}},"3b7a14ac-3224-4b9a-b474-f6136a658f8b"],[{"index":{"value":124},"size":{"value":6}},"7f7f18a4-06ef-46e0-bddd-b9fe60878855"],[{"index":{"value":124},"size":{"value":29}},"d49e0fe3-a638-4d7e-ba58-12db7abad20f"],[{"index":{"value":131},"size":{"value":8}},"c9411433-5e73-4927-9175-15d3b1ccb0ef"],[{"index":{"value":131},"size":{"value":22}},"b53ad817-ea4c-491e-aaf8-49967e22aff6"],[{"index":{"value":139},"size":{"value":1}},"837a4d65-8af6-4860-ae0e-ad9172af6ad2"],[{"index":{"value":140},"size":{"value":13}},"3b662906-1887-4dd6-a0e2-a222ace135ce"],[{"index":{"value":154},"size":{"value":6}},"548ca873-544f-48c9-b7ab-6aa1e7f03d72"],[{"index":{"value":154},"size":{"value":38}},"7c45872c-22b4-4f37-bcd7-b3791408f737"],[{"index":{"value":161},"size":{"value":8}},"21152f0d-4222-48e4-9376-a2a28f8f6be6"],[{"index":{"value":161},"size":{"value":22}},"1cb71d49-a06d-4dd9-bc1b-54603b416601"],[{"index":{"value":161},"size":{"value":31}},"faa46cf2-3e90-44ed-9d9e-77b7f874b338"],[{"index":{"value":169},"size":{"value":1}},"6d5fdcd5-0ddf-4c83-aafe-a446ac5ce461"],[{"index":{"value":170},"size":{"value":13}},"830d73c2-1898-41a9-8046-22d86f159ef0"],[{"index":{"value":183},"size":{"value":1}},"ab897380-c596-4182-a2c5-b7b23cf59893"],[{"index":{"value":184},"size":{"value":8}},"29f35cf2-d79d-44d0-96d0-79b11589726c"],[{"index":{"value":193},"size":{"value":4}},"45bf8220-52f2-49f4-bbcf-9566d1f1b1c4"],[{"index":{"value":193},"size":{"value":57}},"478005ac-f0ca-497a-b298-554bd64946a9"],[{"index":{"value":198},"size":{"value":8}},"c11511ce-252c-4b84-9f61-dfa5544d4916"],[{"index":{"value":198},"size":{"value":13}},"dbae65a1-bdca-44b0-a497-7934d1e42616"],[{"index":{"value":198},"size":{"value":20}},"355392ad-8c81-4304-a6d0-2bbf3690da0c"],[{"index":{"value":198},"size":{"value":27}},"0720ccb5-ebd3-4c29-8113-0aa1f251e873"],[{"index":{"value":206},"size":{"value":1}},"f5a8d310-1f58-456e-84a3-78490f05ff13"],[{"index":{"value":207},"size":{"value":4}},"10046e78-5164-407c-adef-2ab11ce77b4c"],[{"index":{"value":211},"size":{"value":1}},"ed23da0c-9193-4eea-ab9c-fee90ba8f924"],[{"index":{"value":212},"size":{"value":6}},"7a09de10-35e2-45e0-9eb8-f79fa054b733"],[{"index":{"value":218},"size":{"value":1}},"b85a0981-9d82-4204-a03a-cd3a4720c72d"],[{"index":{"value":219},"size":{"value":6}},"1b04c19d-cdf0-4661-8f4e-d980cf12c996"],[{"index":{"value":226},"size":{"value":6}},"87d57f2c-e009-48a7-98bb-0e1612905564"],[{"index":{"value":233},"size":{"value":17}},"60fd6a1c-7ada-4546-ae20-e1f8b432b4fd"],[{"index":{"value":252},"size":{"value":4}},"9558affa-b913-4576-8ee3-6af22985e0f6"],[{"index":{"value":252},"size":{"value":32}},"8479091c-9232-49d6-b375-32f89693c389"],[{"index":{"value":257},"size":{"value":7}},"3d1f13f5-e22a-4cb6-9e75-9c717e016922"],[{"index":{"value":264},"size":{"value":1}},"aa429f07-973a-4c69-8e3f-74aa4780f85a"],[{"index":{"value":269},"size":{"value":5}},"5b304cf6-5ec4-4b23-ad0d-20e38b41cdcd"],[{"index":{"value":269},"size":{"value":15}},"e3315ef2-d0ec-43b1-b8f6-5b436054aaee"],[{"index":{"value":275},"size":{"value":2}},"bc7205c4-68bf-461e-a1ba-31400b9337ff"],[{"index":{"value":278},"size":{"value":6}},"78870c05-9117-4e24-bf77-a72e7aef9c18"],[{"index":{"value":286},"size":{"value":4}},"517c743f-c775-4031-8f87-222d0f0a365f"],[{"index":{"value":286},"size":{"value":271}},"d0d107bc-4997-41d6-9c20-32295590eaac"],[{"index":{"value":291},"size":{"value":1}},"91174930-f18b-4322-84cc-deb88637d012"],[{"index":{"value":292},"size":{"value":265}},"f09a4372-3231-4f2e-99f4-84aa751f9b60"],[{"index":{"value":297},"size":{"value":13}},"613df8c9-0a40-4c94-886f-9668c2c360c2"],[{"index":{"value":297},"size":{"value":19}},"5b88a4d4-6840-4a25-a862-8b5d0b75303d"],[{"index":{"value":311},"size":{"value":1}},"6bfe1dea-09df-4358-9613-74d8326bc680"],[{"index":{"value":313},"size":{"value":3}},"e3b7fac7-0c08-4f20-8a0c-a58f1b118097"],[{"index":{"value":321},"size":{"value":13}},"37d007fe-9cb6-4f2e-a8b7-30842eac60b2"],[{"index":{"value":321},"size":{"value":22}},"4233204a-543d-437a-a9fc-9f79853a9540"],[{"index":{"value":335},"size":{"value":1}},"6c01783b-27ba-413c-84a4-6a94a37d714a"],[{"index":{"value":337},"size":{"value":1}},"34bc865a-58b5-4c49-bf85-a8cfedaf9b54"],[{"index":{"value":337},"size":{"value":6}},"bb556514-570d-4a87-8b2a-6e7f198a975f"],[{"index":{"value":338},"size":{"value":4}},"87c2cedc-898e-43c3-be3d-24e15689c373"],[{"index":{"value":342},"size":{"value":1}},"13387ff5-ec5e-453a-86f2-deae721b5549"],[{"index":{"value":348},"size":{"value":13}},"6280a86b-5469-43f3-94d5-1e1726028a54"],[{"index":{"value":348},"size":{"value":57}},"a17c5e55-5e99-4e4f-961d-9b4072ecd6e4"],[{"index":{"value":362},"size":{"value":1}},"c0720382-9ce4-451a-a04b-f248143d2528"],[{"index":{"value":364},"size":{"value":7}},"9cd51399-d9d2-4898-9bcb-be2f540999ff"],[{"index":{"value":364},"size":{"value":13}},"b4980bfc-13cc-429f-bc43-9edfc07d2406"],[{"index":{"value":364},"size":{"value":27}},"1a3f6a8c-ab27-43dd-a494-bf413334fd9a"],[{"index":{"value":364},"size":{"value":41}},"71a97e88-9b19-4ec9-b3c8-ffd2940c4cb8"],[{"index":{"value":371},"size":{"value":1}},"99f38c61-3ea7-4b02-be71-f293fbecb7d7"],[{"index":{"value":372},"size":{"value":5}},"08820818-ddb9-408a-b913-e9382e5b6dc7"],[{"index":{"value":378},"size":{"value":13}},"2cf06b4e-af9f-492e-ac13-5723a896a508"],[{"index":{"value":392},"size":{"value":13}},"6c887279-5998-45ce-81ab-c37fdeb03144"],[{"index":{"value":410},"size":{"value":13}},"6c126d22-c018-46cc-bdae-cf8b89104173"],[{"index":{"value":410},"size":{"value":40}},"a65c1fa8-9fce-4c30-bef7-d85ff1b428eb"],[{"index":{"value":424},"size":{"value":1}},"208c049e-e221-49ce-99d7-96142f2d1b1c"],[{"index":{"value":426},"size":{"value":13}},"0adf4ddd-1402-4b96-bfb0-1917cc275063"],[{"index":{"value":426},"size":{"value":19}},"f2158ac6-2dd5-482d-9f3d-0bf4812d6d6e"],[{"index":{"value":426},"size":{"value":24}},"c7bbc3e7-1377-429e-b60a-0c2d2cd4ea74"],[{"index":{"value":439},"size":{"value":1}},"1a17a7ee-31d7-4885-84ce-c902bff4ea4a"],[{"index":{"value":440},"size":{"value":5}},"47016cac-40a0-45f6-8057-2b81edb052a5"],[{"index":{"value":446},"size":{"value":4}},"ce4fa4cd-5064-4752-9dbc-5fcf4e92138a"],[{"index":{"value":455},"size":{"value":13}},"3300001d-13ea-45f4-acbe-959813bcf85b"],[{"index":{"value":455},"size":{"value":17}},"b8f5900b-2370-4b21-8a0c-06d3bbb45068"],[{"index":{"value":469},"size":{"value":1}},"19ea3187-83d6-47d1-994c-64cf0ce9af3b"],[{"index":{"value":471},"size":{"value":1}},"9457c3f8-c878-4f01-a5c7-39336f163a28"],[{"index":{"value":477},"size":{"value":13}},"a5139627-05a8-4838-b171-02666c62c348"],[{"index":{"value":477},"size":{"value":80}},"8b5c864c-055d-4d11-a470-2679328fc131"],[{"index":{"value":491},"size":{"value":1}},"eb01b0af-06cf-4d75-81ad-d0bf7664b2d7"],[{"index":{"value":493},"size":{"value":13}},"6c042eb7-e895-473d-b456-6e0eec23e958"],[{"index":{"value":493},"size":{"value":18}},"f40730ff-ac6c-45db-8970-534f3becda2a"],[{"index":{"value":493},"size":{"value":64}},"b2df8ba8-683c-45c2-9932-8e1c970c799a"],[{"index":{"value":506},"size":{"value":1}},"2289bf31-74dd-431c-97f1-3ac179715453"],[{"index":{"value":507},"size":{"value":4}},"6db4523a-0bcd-4ba5-b6c2-7720db57d93c"],[{"index":{"value":512},"size":{"value":1}},"b6faea17-664d-470a-9641-999b6a2be5bd"],[{"index":{"value":512},"size":{"value":45}},"98aea277-bc44-4ceb-85a6-24ee453b7f72"],[{"index":{"value":513},"size":{"value":15}},"c0ae1c05-026d-4712-a571-979ce260b310"],[{"index":{"value":513},"size":{"value":24}},"502e8e4d-5105-4abe-8dba-4d7293cbe80b"],[{"index":{"value":513},"size":{"value":43}},"71cee748-378b-4817-be0e-b87bc1d3d847"],[{"index":{"value":528},"size":{"value":1}},"fcaad3ac-e080-4170-9d63-b2c01295447d"],[{"index":{"value":529},"size":{"value":8}},"a053e05a-4862-4407-abe8-2746784bdfc3"],[{"index":{"value":538},"size":{"value":1}},"9adae78d-7667-4865-83e9-b64464b13696"],[{"index":{"value":538},"size":{"value":18}},"92f153e3-da05-439e-bb4f-596ec8cfa9c8"],[{"index":{"value":539},"size":{"value":1}},"dddedc5e-415d-4f5d-ab1b-19468aa079a9"],[{"index":{"value":540},"size":{"value":1}},"4adce2e5-1e60-4a63-9ff3-5781d967b704"],[{"index":{"value":542},"size":{"value":13}},"ef06ed1e-246f-4106-a4c0-1e648b8ad5e7"],[{"index":{"value":555},"size":{"value":1}},"7bd1cc5f-20b1-4dd2-8a34-ccc40f5f091a"],[{"index":{"value":556},"size":{"value":1}},"720adbd6-24b0-49ac-b123-61cc037a9636"]]
{"ide":{"node":{"e3b7fac7-0c08-4f20-8a0c-a58f1b118097":{"position":{"vector":[-224,13]}},"bb556514-570d-4a87-8b2a-6e7f198a975f":{"position":{"vector":[115,19]}},"71a97e88-9b19-4ec9-b3c8-ffd2940c4cb8":{"position":{"vector":[-224,-51]}},"c7bbc3e7-1377-429e-b60a-0c2d2cd4ea74":{"position":{"vector":[360,19]},"visualization":{"show":false,"fullscreen":false,"width":200}},"f2158ac6-2dd5-482d-9f3d-0bf4812d6d6e":{"position":{"vector":[297,-45]},"visualization":{"show":false,"fullscreen":false,"width":96}},"9457c3f8-c878-4f01-a5c7-39336f163a28":{"position":{"vector":[640,40]},"visualization":{"show":false,"fullscreen":false,"width":200}},"b2df8ba8-683c-45c2-9932-8e1c970c799a":{"position":{"vector":[219,-80]},"visualization":{"show":true,"fullscreen":false,"width":200}},"f40730ff-ac6c-45db-8970-534f3becda2a":{"position":{"vector":[219,-48]}}},"import":{}}}
```

To test the functionality, I asked AI to show me the result of the `operator70395` variable:

1. Init protocol connection
```json
{"jsonrpc":"2.0","id":0,"method":"session/initProtocolConnection","params":{"clientId":"d8e948fd-6418-43c8-9f02-54827f09e10a"}}
```

2. Create execution context
```json
{"jsonrpc":"2.0","id":0,"method":"session/initProtocolConnection","params":{"clientId":"d8e948fd-6418-43c8-9f02-54827f09e10a"}}
```

3. Push the main method
```json
{"jsonrpc":"2.0","id":0,"method":"executionContext/push","params":{"contextId":"730a66ef-4222-46f8-8a03-d766946ab2bd","stackItem":{"methodPointer":{"module":"local.New_Project_1.Main","definedOnType":"local.New_Project_1.Main","name":"main"},"positionalArgumentsExpressions":[],"type":"ExplicitCall"}}}
```

4. Ask AI for the variable contents
```json
{"jsonrpc":"2.0","id":1,"method":"ai/completion_v2","params":{"contextId":"730a66ef-4222-46f8-8a03-d766946ab2bd","expressionId":"f09a4372-3231-4f2e-99f4-84aa751f9b60","prompt":"There is 'operator70395' variable defined in the program. What is the result of the variable 'operator70395'?"}}
```

I got the following responses:
```json
{"jsonrpc":"2.0","method":"ai/completionProgress","params":{"code":"Visualization.AI.print(operator70395)","reason":"To provide the result of 'operator70395', I need to know its current value.","visualizationId":"edfb00a3-6ce5-41e1-bb8f-ab191809114e"}}
```
```json
{"jsonrpc":"2.0","id":1,"result":{"Success":{"fn":"def get_operator70395_result():\n    return operator70395","fnCall":"get_operator70395_result()"}}}
```
This commit is contained in:
Dmitry Bushev 2024-05-02 17:55:06 +01:00 committed by GitHub
parent c36cd87b99
commit a44bb2b1b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 725 additions and 39 deletions

View File

@ -1,5 +1,6 @@
from Standard.Base import all
from Standard.Table import Table
import project.Helpers
## PRIVATE
goal_placeholder = "__$$GOAL$$__"
@ -34,3 +35,6 @@ Table.build_ai_prompt self =
## PRIVATE
build_ai_prompt subject = subject.build_ai_prompt
## PRIVATE
print subject = subject.to_default_visualization_data

View File

@ -187,6 +187,9 @@ transport formats, please look [here](./protocol-architecture).
- [Profiling Operations](#profiling-operations)
- [`profiling/start`](#profilingstart)
- [`profiling/stop`](#profilingstop)
- [AI Operations](#ai-operations)
- [`ai/completion_v2`](#aicompletionv2)
- [`ai/completionProgress`](#aicompletionprogres)
- [Errors](#errors-75)
- [`Error`](#error)
- [`AccessDeniedError`](#accessdeniederror)
@ -4296,7 +4299,7 @@ interface SearchGetSuggestionsDatabaseVersionResult {
### `search/suggestionsDatabaseUpdate`
Sent from server to the client to inform abouth the change in the suggestions
Sent from server to the client to inform about the change in the suggestions
database.
- **Type:** Notification
@ -4315,7 +4318,7 @@ interface SearchSuggestionsDatabaseUpdateNotification {
### `search/suggestionsOrderDatabaseUpdate`
Sent from server to the client to inform abouth the change in the suggestions
Sent from server to the client to inform about the change in the suggestions
order database.
- **Type:** Notification
@ -5178,6 +5181,92 @@ interface ProfilingStopResult {}
None
## AI Operations
### `ai/completion_v2`
Sent from the client to the server to ask the AI model the code suggestion.
- **Type:** Request
- **Direction:** Client -> Server
- **Connection:** Protocol
- **Visibility:** Public
#### Parameters
```typescript
interface AiCompletionParameters {
/** The execution context id to use for executing expressions. */
contextId: UUID;
/**
* The expression providing the execution scope. The same as `expressionId`
* parameter of `executionContext/executeExpression` method.
*/
expressionId: UUID;
/** The user prompt. */
prompt: string;
/** The system prompt describing the AI role. */
systemPrompt?: string;
/** The AI model to use. */
model?: string;
}
```
#### Result
```typescript
type AiCompletionResult = AiCompletionResultSuccess | AiCompletionResultFailure;
interface AiCompletionResultSuccess {
/** The code of the function producing the desired result. */
fn: string;
/** The code of how to call the suggested function. */
fnCall: string;
}
interface AiCompletionResultFailure {
/**
* The explanation given by the AI model for why it was unable to provide the
* answer.
*/
reason: string;
}
```
#### Errors
- [`AiHttpError`](#aihttperror) Signals about an error during the processing of
AI http respnse.
- [`AiEvaluationError`](#aievaluationerror) Signals about an error during the
evaluation of expression requested by AI.
### `ai/completionProgress`
Sent from server to the client to inform about the progress of the
[`ai/completion`](#aicompletion) request.
- **Type:** Notification
- **Direction:** Server -> Client
- **Connection:** Protocol
- **Visibility:** Public
#### Notification
```typescript
interface AiCompletionProgressNotification {
/** Code snippte that AI model requested to evaluate. */
code: string;
/** Explanation given by the AI model why it needs an extra information. */
reason: string;
/**
* The id of the visualization being executed. When evaluated, the
* visualization update will contain the result of the executed expression.
*/
visualizationId: UUID;
}
```
## Errors
The language server component also has its own set of errors. This section is
@ -5763,3 +5852,34 @@ Signals that the refactoring of the given expression is not supported.
"message" : "Refactoring not supported for expression [<expression-id>]"
}
```
### `AiHttpError`
Signals about an error during the processing of AI http respnse.
```typescript
"error" : {
"code" : 10001,
"message" : "Failed to process HTTP response",
"payload" : {
"reason" : "<Failure reason>",
"request" : "<HTTP request sent>",
"response" : "<HTTP response received>"
}
}
```
### `AiEvaluationError`
Signals about an error during the evaluation of expression requested by AI.
```typescript
"error" : {
"code" : 10002,
"message" : "Failed to execute expression",
"payload" : {
"expression" : "<Evaluated expression>",
"error" : "<The evaluation error message>"
}
}
```

View File

@ -1,18 +0,0 @@
package org.enso.languageserver.ai
import org.enso.jsonrpc.{HasParams, HasResult, Method}
case object AICompletion extends Method("ai/completion") {
case class Params(prompt: String, stopSequence: String)
case class Result(code: String)
implicit val hasParams: HasParams.Aux[this.type, AICompletion.Params] =
new HasParams[this.type] {
type Params = AICompletion.Params
}
implicit val hasResult: HasResult.Aux[this.type, AICompletion.Result] =
new HasResult[this.type] {
type Result = AICompletion.Result
}
}

View File

@ -0,0 +1,82 @@
package org.enso.languageserver.ai
import io.circe.Json
import io.circe.syntax._
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method}
import java.util.UUID
case object AiApi {
case object AiCompletion extends Method("ai/completion") {
case class Params(prompt: String, stopSequence: String)
case class Result(code: String)
implicit val hasParams: HasParams.Aux[this.type, AiCompletion.Params] =
new HasParams[this.type] {
type Params = AiCompletion.Params
}
implicit val hasResult: HasResult.Aux[this.type, AiCompletion.Result] =
new HasResult[this.type] {
type Result = AiCompletion.Result
}
}
case object AiCompletion2 extends Method("ai/completion_v2") {
case class Params(
contextId: UUID,
expressionId: UUID,
prompt: String,
systemPrompt: Option[String],
model: Option[String]
)
type Result = AiProtocol.AiCompletionResult
implicit val hasParams: HasParams.Aux[this.type, AiCompletion2.Params] =
new HasParams[this.type] {
type Params = AiCompletion2.Params
}
implicit val hasResult: HasResult.Aux[this.type, AiCompletion2.Result] =
new HasResult[this.type] {
type Result = AiCompletion2.Result
}
}
case object AiCompletionProgress extends Method("ai/completionProgress") {
case class Params(code: String, reason: String, visualizationId: UUID)
implicit
val hasParams: HasParams.Aux[this.type, AiCompletionProgress.Params] =
new HasParams[this.type] {
type Params = AiCompletionProgress.Params
}
}
case class AiHttpError(reason: String, request: Json, response: String)
extends Error(10001, "Failed to process HTTP response") {
override val payload: Option[Json] = Some(
Json.obj(
("reason", reason.asJson),
("request", request),
("response", response.asJson)
)
)
}
case class AiEvaluationError(expression: String, error: String)
extends Error(10002, "Failed to execute expression") {
override val payload: Option[Json] = Some(
Json.obj(
("expression", expression.asJson),
("error", error.asJson)
)
)
}
}

View File

@ -0,0 +1,53 @@
package org.enso.languageserver.ai
import java.util.UUID
object AiProtocol {
/** Base trait for the AI completion results. */
sealed trait AiCompletionResult
case object AiCompletionResult {
/** Successful completion result.
*
* @param fn the code for the function returning the answer
* @param fnCall the code how to call the function
*/
sealed case class Success(fn: String, fnCall: String)
extends AiCompletionResult
/** Failed completion result
*
* @param reason the explanation why the AI was unable to provide the result.
*/
sealed case class Failure(reason: String) extends AiCompletionResult
}
/** The request from AI to evaluate an expression.
*
* @param reason the explanation why the AI requires this information
* @param code the expression code
*/
case class AiEvalRequest(reason: String, code: String)
/** The message sent to AI.
*
* @param role the AI role
* @param content the message content
*/
case class CompletionsMessage(role: String, content: String)
/** The progress notification sent when AI requests to evaluate an expression.
*
* @param code the code that AI requested to evaluate
* @param reason the explanation why AI requires this information
* @param visualizationId the id of the visualization being executed. When evaluated,
* the visualization update will contain the result of the executed expression.
*/
case class AiCompletionProgressNotification(
code: String,
reason: String,
visualizationId: UUID
)
}

View File

@ -7,7 +7,12 @@ import com.typesafe.scalalogging.LazyLogging
import org.enso.cli.task.ProgressUnit
import org.enso.cli.task.notifications.TaskNotificationApi
import org.enso.jsonrpc._
import org.enso.languageserver.ai.AICompletion
import org.enso.languageserver.ai.AiApi.{
AiCompletion,
AiCompletion2,
AiCompletionProgress
}
import org.enso.languageserver.ai.AiProtocol
import org.enso.languageserver.boot.resource.{
InitializationComponent,
InitializationComponentInitialized
@ -472,6 +477,16 @@ class JsonConnectionController(
translateProgressNotification(payload)
webActor ! translated
case AiProtocol.AiCompletionProgressNotification(
code,
reason,
visualizationId
) =>
webActor ! Notification(
AiCompletionProgress,
AiCompletionProgress.Params(code, reason, visualizationId)
)
case req @ Request(method, _, _) if requestHandlers.contains(method) =>
refreshIdleTime(method)
val handler = context.actorOf(
@ -566,9 +581,14 @@ class JsonConnectionController(
.props(requestTimeout, suggestionsHandler),
InvalidateSuggestionsDatabase -> search.InvalidateSuggestionsDatabaseHandler
.props(requestTimeout, suggestionsHandler),
AICompletion -> ai.AICompletionHandler.props(
AiCompletion -> ai.AICompletionHandler.props(
languageServerConfig.aiCompletionConfig
),
AiCompletion2 -> ai.AICompletion2Handler.props(
languageServerConfig.aiCompletionConfig,
rpcSession,
runtimeConnector
),
ExecuteExpression -> ExecuteExpressionHandler
.props(rpcSession.clientId, requestTimeout, contextRegistry),
AttachVisualization -> AttachVisualizationHandler

View File

@ -7,7 +7,11 @@ import org.enso.cli.task.notifications.TaskNotificationApi.{
TaskStarted
}
import org.enso.jsonrpc.Protocol
import org.enso.languageserver.ai.AICompletion
import org.enso.languageserver.ai.AiApi.{
AiCompletion,
AiCompletion2,
AiCompletionProgress
}
import org.enso.languageserver.capability.CapabilityApi.{
AcquireCapability,
ForceReleaseCapability,
@ -88,7 +92,8 @@ object JsonRpc {
.registerRequest(GetSuggestionsDatabaseVersion)
.registerRequest(InvalidateSuggestionsDatabase)
.registerRequest(Completion)
.registerRequest(AICompletion)
.registerRequest(AiCompletion)
.registerRequest(AiCompletion2)
.registerRequest(RenameProject)
.registerRequest(RenameSymbol)
.registerRequest(ProjectInfo)
@ -131,5 +136,6 @@ object JsonRpc {
.registerNotification(SuggestionsDatabaseUpdates)
.registerNotification(VisualizationEvaluationFailed)
.registerNotification(ProjectRenamed)
.registerNotification(AiCompletionProgress)
.finalized()
}

View File

@ -0,0 +1,19 @@
package org.enso.languageserver.requesthandler
import akka.actor.Actor
import com.typesafe.scalalogging.LazyLogging
import org.enso.jsonrpc.{Errors, Method, Request, ResponseError}
import org.enso.languageserver.util.UnhandledLogging
final class UnsupportedHandler(method: Method)
extends Actor
with LazyLogging
with UnhandledLogging {
override def receive: Receive = { case Request(`method`, id, _) =>
sender() ! ResponseError(
Some(id),
Errors.MethodNotFound
)
}
}

View File

@ -0,0 +1,409 @@
package org.enso.languageserver.requesthandler.ai
import akka.actor.{Actor, ActorRef, PoisonPill, Props}
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers.OAuth2BearerToken
import akka.pattern.PipeToSupport
import akka.stream.Materializer
import akka.util.ByteString
import com.typesafe.scalalogging.LazyLogging
import io.circe.Json
import io.circe.syntax._
import io.circe.generic.auto._
import org.enso.jsonrpc._
import org.enso.languageserver.ai.AiApi.{
AiCompletion2,
AiEvaluationError,
AiHttpError
}
import org.enso.languageserver.ai.AiProtocol
import org.enso.languageserver.data.AICompletionConfig
import org.enso.languageserver.requesthandler.UnsupportedHandler
import org.enso.languageserver.runtime.{
ContextRegistryProtocol,
RuntimeFailureMapper
}
import org.enso.languageserver.session.JsonSession
import org.enso.languageserver.util.UnhandledLogging
import org.enso.logger.akka.ActorMessageLogging
import org.enso.polyglot.runtime.Runtime.Api
import java.nio.charset.StandardCharsets
import java.util.UUID
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.FiniteDuration
class AICompletion2Handler(
cfg: AICompletionConfig,
session: JsonSession,
runtime: ActorRef
) extends Actor
with LazyLogging
with ActorMessageLogging
with UnhandledLogging
with PipeToSupport {
import AICompletion2Handler._
override def preStart(): Unit = {
super.preStart()
context.system.eventStream.subscribe(self, classOf[Api.VisualizationUpdate])
context.system.eventStream
.subscribe(self, classOf[Api.VisualizationEvaluationFailed])
}
override def receive: Receive = requestStage
private val http = Http(context.system)
implicit val ec: ExecutionContext = context.dispatcher
implicit val materializer: Materializer = Materializer(context)
private def requestStage: Receive = LoggingReceive.withLabel("requestStage") {
case Request(
AiCompletion2,
id,
AiCompletion2.Params(
contextId,
expressionId,
prompt,
systemPrompt,
model
)
) =>
val messages = Vector(
AiProtocol.CompletionsMessage(
"system",
systemPrompt.getOrElse(SYSTEM_PROMPT)
),
AiProtocol.CompletionsMessage("user", prompt)
)
val httpReq = sendHttpRequest(messages, model)
val debugInfo = DebugInfo(httpReq)
context.become(
awaitingCompletionResponse(
id,
sender(),
contextId,
expressionId,
messages,
model,
debugInfo
)
)
}
private def evalRequestStage(
id: Id,
replyTo: ActorRef,
contextId: Api.ContextId,
expressionId: Api.ExpressionId,
messages: Vector[AiProtocol.CompletionsMessage],
model: Option[String]
): Receive = LoggingReceive.withLabel("evalRequestStage") {
case req @ AiProtocol.AiEvalRequest(reason, code) =>
val requestId = UUID.randomUUID()
val visualizationId = UUID.randomUUID()
val executeExpression = Api.ExecuteExpression(
contextId,
visualizationId,
expressionId,
code
)
runtime ! Api.Request(requestId, executeExpression)
session.rpcController ! AiProtocol.AiCompletionProgressNotification(
code,
reason,
visualizationId
)
context.become(
evalResponseStage(
id,
replyTo,
contextId,
expressionId,
visualizationId,
req,
messages,
model
)
)
}
private def evalResponseStage(
id: Id,
replyTo: ActorRef,
contextId: Api.ContextId,
expressionId: Api.ExpressionId,
visualizationId: Api.VisualizationId,
request: AiProtocol.AiEvalRequest,
messages: Vector[AiProtocol.CompletionsMessage],
model: Option[String]
): Receive = LoggingReceive.withLabel("evalResponseStage") {
case Api.VisualizationUpdate(ctx, data)
if ctx.visualizationId == visualizationId =>
val visualizationResult = new String(data, StandardCharsets.UTF_8)
val message = AiProtocol.CompletionsMessage(
"user",
s"EVALUATED:\n${request.code}\n\nOUTPUT:\n$visualizationResult"
)
val newMessages = messages :+ message
val httpReq = sendHttpRequest(newMessages, model)
val debugInfo = DebugInfo(httpReq)
context.become(
awaitingCompletionResponse(
id,
replyTo,
contextId,
expressionId,
newMessages,
model,
debugInfo
)
)
case Api.VisualizationEvaluationFailed(ctx, message, _)
if ctx.visualizationId == visualizationId =>
val aiError = AiEvaluationError(request.code, message)
replyTo ! ResponseError(Some(id), aiError)
stop()
case error: ContextRegistryProtocol.Failure =>
replyTo ! ResponseError(Some(id), RuntimeFailureMapper.mapFailure(error))
}
private def awaitingCompletionResponse(
id: Id,
replyTo: ActorRef,
contextId: Api.ContextId,
expressionId: Api.ExpressionId,
messages: Vector[AiProtocol.CompletionsMessage],
model: Option[String],
debugInfo: DebugInfo
): Receive = LoggingReceive.withLabel("awaitingCompletionStage") {
case HttpResponse(StatusCodes.OK, data) =>
val responseUtf8String = data.utf8String
logger.trace("AI response:\n{}", responseUtf8String)
parse(responseUtf8String) match {
case Some(response) =>
getResponseKind(response) match {
case Some("final") =>
getFinalResult(response).fold {
val aiError = AiHttpError(
"Failed to parse final kind of AI response",
debugInfo.httpReq,
responseUtf8String
)
replyTo ! ResponseError(Some(id), aiError)
}(success => replyTo ! ResponseResult(AiCompletion2, id, success))
stop()
case Some("eval") =>
getEvalResult(response).fold {
val aiError = AiHttpError(
"Failed to parse eval kind of AI response",
debugInfo.httpReq,
responseUtf8String
)
replyTo ! ResponseError(Some(id), aiError)
stop()
} { evalRequest =>
self ! evalRequest
context.become(
evalRequestStage(
id,
replyTo,
contextId,
expressionId,
messages,
model
)
)
}
case Some("fail") =>
getFailResult(response).fold {
val aiError = AiHttpError(
"Failed to parse fail kind of AI response",
debugInfo.httpReq,
responseUtf8String
)
replyTo ! ResponseError(Some(id), aiError)
}(fail => replyTo ! ResponseResult(AiCompletion2, id, fail))
stop()
case _ =>
val aiError = AiHttpError(
"Unknown kind of AI response",
debugInfo.httpReq,
responseUtf8String
)
replyTo ! ResponseError(Some(id), aiError)
stop()
}
case None =>
val aiError = AiHttpError(
"Failed to parse AI response as JSON",
debugInfo.httpReq,
data.utf8String
)
replyTo ! ResponseError(Some(id), aiError)
stop()
}
case HttpResponse(status, data) =>
val aiError =
AiHttpError(
s"Unknown AI response [${status.value}]",
debugInfo.httpReq,
data.utf8String
)
replyTo ! ResponseError(Some(id), aiError)
stop()
}
private def sendHttpRequest(
messages: Vector[AiProtocol.CompletionsMessage],
modelOption: Option[String]
): Json = {
val body = Json.obj(
("model", modelOption.getOrElse(MODEL).asJson),
("response_format", Json.obj(("type", "json_object".asJson))),
("messages", Json.arr(messages.map(_.asJson): _*))
)
logger.trace("AI request:\n{}", body)
val req =
HttpRequest(
uri = API_OPENAI_URI,
method = HttpMethods.POST,
headers = Seq(headers.Authorization(OAuth2BearerToken(cfg.apiKey))),
entity = HttpEntity(ContentTypes.`application/json`, body.noSpaces)
)
http
.singleRequest(req)
.flatMap(response => {
response.entity
.toStrict(FiniteDuration(10, "s"))
.map(e => {
HttpResponse(response.status, e.data)
})
})
.pipeTo(self)
body
}
private def stop(): Unit = {
self ! PoisonPill
}
}
object AICompletion2Handler {
private val MODEL = "gpt-4-turbo-preview"
private val API_OPENAI_URI = "https://api.openai.com/v1/chat/completions"
private val SYSTEM_PROMPT =
"""You are a data analyst. You use Python3. Installed libraries: ['pandas'].
|Your task is to output JSON object with fields:
|- 'kind': 'final'
|- 'fn': String, Python function returning what user wants. Always write as generic code as possible that will work even if the input data (e.g. file content) changes. Do not assume any input data exists if not provided with it explicitly. Use your knowledge about the world if the provided data is missing.
|- 'fnCall': String, Python code that calls the generated function.
|- 'resultPreview': Code in Python. When evaluated, prints to stdout a preview of the result, e.g. 'Visualization.AI.print("Number 5")'. Make it as generic as possible. It should work even if the input data changes. The string written to stdout should be one-line, as short as possible, and as informative as possible, e.g. 'Table with 50 rows and columns "c1", "c2", and "c3"'. It can assume that the 'fnCall' result is in scope.
|- 'queryParts': Array of user query divided into either non-editable text, or widgets. The idea is that users can click widgets to change them to other values. Every part should be one of the JSON objects:
| * Fields:
| a. 'kind': 'text'
| b. 'text': Part of the user query that should not be widget. In particular, numbers shoul not be widgets.
| * Fields:
| a. 'kind': 'dropdown'
| b. 'values': list of possible values. For example, if the query contains name of a column in a data set, provide all other column names, like ['columnName1', 'columnName2']. If the query contains a common comparator like 'less than', provide other comparators like ['greater than', 'equal to']. The same applies to other comparators like 'most popular'.
|
|If in order to provide the answer you need to investigate what is inside the data, you can run code and be asked again the same question with provided stdout by outputing JSON object with fields:
|- 'kind': 'eval'
|- 'code': Python code required to investigate data. The code should write to stdout as little as possible. Use 'Visualization.AI.print' function for printing to stdout. You can only use data you are already provided with, no more data can be provided and you can't ask for more data.
|- 'reason': Reason why you were not able to provide final code.
|Always prefer outputting the final code. Use kind "eval" only if you can't output object with kind "final".
|
|If you can't provide the answer, because the current data and your knowledge about the world is not enough, output JSON object with fields:
|- 'kind': 'fail'
|- 'reason': Reason why you were not able to provide the answer. As short as possible. Do not mention Python nor code, this is information for non-tech users.
|""".stripMargin
private case class HttpResponse(status: StatusCode, data: ByteString)
private case class DebugInfo(
httpReq: Json
)
def props(
cfg: Option[AICompletionConfig],
session: JsonSession,
runtime: ActorRef
): Props =
cfg
.map(conf => Props(new AICompletion2Handler(conf, session, runtime)))
.getOrElse(Props(new UnsupportedHandler(AiCompletion2)))
private def parse(str: String): Option[Json] =
for {
response <- io.circe.parser.parse(str).toOption
responseObj <- response.asObject
choices <- responseObj("choices")
choicesArr <- choices.asArray
firstChoice <- choicesArr.headOption
firstChoiceObj <- firstChoice.asObject
message <- firstChoiceObj("message")
messageObj <- message.asObject
content <- messageObj("content")
contentString <- content.asString
contentJson <- io.circe.parser.parse(contentString).toOption
} yield contentJson
private def getFinalResult(
response: Json
): Option[AiProtocol.AiCompletionResult] =
for {
obj <- response.asObject
fn <- obj("fn")
fnString <- fn.asString
fnCall <- obj("fnCall")
fnCallString <- fnCall.asString
} yield AiProtocol.AiCompletionResult.Success(fnString, fnCallString)
private def getEvalResult(response: Json): Option[AiProtocol.AiEvalRequest] =
for {
obj <- response.asObject
reason <- obj("reason")
reasonString <- reason.asString
code <- obj("code")
codeString <- code.asString
} yield AiProtocol.AiEvalRequest(reasonString, codeString)
private def getFailResult(
response: Json
): Option[AiProtocol.AiCompletionResult] =
for {
obj <- response.asObject
reason <- obj("reason")
reasonString <- reason.asString
} yield AiProtocol.AiCompletionResult.Failure(reasonString)
private def getResponseKind(response: Json): Option[String] =
for {
obj <- response.asObject
key <- obj("kind")
keyString <- key.asString
} yield keyString
}

View File

@ -3,7 +3,7 @@ package org.enso.languageserver.requesthandler.ai
import akka.actor.{Actor, ActorRef, Props}
import com.typesafe.scalalogging.LazyLogging
import org.enso.jsonrpc.{Errors, Id, Request, ResponseError, ResponseResult}
import org.enso.languageserver.ai.AICompletion
import org.enso.languageserver.ai.AiApi.AiCompletion
import org.enso.languageserver.util.UnhandledLogging
import akka.http.scaladsl.model._
import akka.http.scaladsl.Http
@ -13,6 +13,7 @@ import akka.stream.Materializer
import akka.util.ByteString
import io.circe.Json
import org.enso.languageserver.data.AICompletionConfig
import org.enso.languageserver.requesthandler.UnsupportedHandler
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.FiniteDuration
@ -31,7 +32,7 @@ class AICompletionHandler(cfg: AICompletionConfig)
implicit val materializer: Materializer = Materializer(context)
private def requestStage: Receive = {
case Request(AICompletion, id, AICompletion.Params(prompt, stop)) =>
case Request(AiCompletion, id, AiCompletion.Params(prompt, stop)) =>
val body = Json.fromFields(
Seq(
("model", Json.fromString("gpt-3.5-turbo-instruct")),
@ -77,9 +78,9 @@ class AICompletionHandler(cfg: AICompletionConfig)
firstChoiceText <- firstChoiceObj("text")
firstChoiceTextStr <- firstChoiceText.asString
} yield ResponseResult(
AICompletion,
AiCompletion,
id,
AICompletion.Result(firstChoiceTextStr)
AiCompletion.Result(firstChoiceTextStr)
)
val handledErrors =
response.getOrElse(ResponseError(Some(id), Errors.ServiceError))
@ -92,16 +93,6 @@ class AICompletionHandler(cfg: AICompletionConfig)
}
}
class UnsupportedHandler extends Actor with LazyLogging with UnhandledLogging {
override def receive: Receive = { case Request(AICompletion, id, _) =>
sender() ! ResponseError(
Some(id),
Errors.MethodNotFound
)
}
}
object AICompletionHandler {
def props(cfg: Option[AICompletionConfig]): Props = cfg
.map(conf =>
@ -109,5 +100,5 @@ object AICompletionHandler {
new AICompletionHandler(conf)
)
)
.getOrElse(Props(new UnsupportedHandler()))
.getOrElse(Props(new UnsupportedHandler(AiCompletion)))
}