Skip to content

Commit 1bec3ee

Browse files
authored
Merge pull request #97 from nemozak1/opeyII_integration
Opey ii integration
2 parents 648d35e + f8b313a commit 1bec3ee

39 files changed

+2221
-8291
lines changed

.env.example

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ VITE_OBP_API_PORTAL_HOST=https://apisandbox.openbankproject.com
88
####################################################################################
99

1010
VITE_OBP_API_VERSION=v5.1.0
11+
#The default version of the root page, it has the default value `OBP+VITE_OBP_API_VERSION`
12+
#The format must follow standard+Version, e.g., OBPv5.1.0, BGv1, or BGv1.3.
13+
#VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION=OBPv5.1.0
1114
VITE_OBP_API_MANAGER_HOST=https://apimanagersandbox.openbankproject.com
1215
VITE_OBP_API_EXPLORER_HOST=http://localhost:5173
1316
VITE_OBP_CONSUMER_KEY=your_consumer_key

.gitignore

+5-2
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ tsconfig.node.tsbuildinfo
3838
vite.config.d.ts
3939
vite.config.js
4040
vitest.config.d.ts
41-
vitest.config.js
4241
components.d.ts
4342

4443
#keys
4544
*.pem
4645
private_key.pem
4746
public_key.pem
48-
./server/cert/*
47+
./server/cert/*
48+
49+
50+
.vite/deps
51+
__snapshots__/

Dockerfiles/frontend_build.env

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
VITE_OBP_API_HOST=VITE_OBP_API_HOST
22
VITE_OBP_API_PORTAL_HOST=VITE_OBP_API_PORTAL_HOST
33
VITE_OBP_API_MANAGER_HOST=VITE_OBP_API_MANAGER_HOST
4-
VITE_OBP_API_VERSION=v5.1.0
5-
4+
VITE_OBP_LOGO_URL=VITE_OBP_LOGO_URL
5+
VITE_OBP_API_VERSION=VITE_OBP_API_VERSION
6+
VITE_OBP_LINKS_COLOR=VITE_OBP_LINKS_COLOR
7+
VITE_OBP_HEADER_LINKS_COLOR=VITE_OBP_HEADER_LINKS_COLOR
8+
VITE_OBP_HEADER_LINKS_HOVER_COLOR=VITE_OBP_HEADER_LINKS_HOVER_COLOR
9+
VITE_OBP_HEADER_LINKS_BACKGROUND_COLOR=VITE_OBP_HEADER_LINKS_BACKGROUND_COLOR
10+
VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION=VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION
11+
VITE_CHATBOT_ENABLED=VITE_CHATBOT_ENABLED
12+
VITE_CHATBOT_URL=VITE_CHATBOT_URL

Dockerfiles/prestart.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,35 @@ import (
77
"path/filepath"
88
"regexp"
99
"strings"
10+
"fmt"
1011
)
1112

13+
// As the frontend environment is read at build time, we need to reprocess the values
14+
// at container runtime.
15+
// This app will search and replace the values set at build time from this build environment: Dockerfiles/frontend_build.env
16+
// with values taken from the container environment.
17+
1218
func main() {
13-
// Define the host env variables to be replaced at build time
14-
config := []string{"VITE_OBP_API_HOST", "VITE_OBP_API_MANAGER_HOST", "VITE_OBP_API_PORTAL_HOST"}
19+
// Define the build env variables to be replaced at container run time
20+
// url config variables are expected to be a valid URL in the container environment
21+
url_config := []string{"VITE_OBP_API_HOST", "VITE_OBP_API_MANAGER_HOST", "VITE_OBP_API_PORTAL_HOST", "VITE_OBP_LOGO_URL"}
22+
// DANGERZONE: The following strings will be replaced by container environment variables without any checking of whatever!!!
23+
config := []string{"VITE_OBP_API_VERSION", "VITE_OBP_LINKS_COLOR", "VITE_OBP_HEADER_LINKS_COLOR", "VITE_OBP_HEADER_LINKS_HOVER_COLOR", "VITE_OBP_HEADER_LINKS_BACKGROUND_COLOR", "VITE_OBP_API_DEFAULT_RESOURCE_DOC_VERSION", "VITE_CHATBOT_ENABLED", "VITE_CHATBOT_URL"}
1524
configMap := make(map[string]string)
1625

1726
for _, key := range config {
27+
value := os.Getenv(key)
28+
if value == "" {
29+
fmt.Printf("Skipping: Environment variable %s is not set\n", key)
30+
continue
31+
}
32+
configMap[key] = value
33+
}
34+
35+
for _, key := range url_config {
1836
rawURL := os.Getenv(key)
1937
if rawURL == "" {
38+
fmt.Printf("Skipping: Environment variable %s is not set\n", key)
2039
continue
2140
}
2241
cleanURL := checkURL(rawURL)

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ openssl rsa -in private_key.pem -pubout -out public_key.pem
161161
cp public_key.pem {path-to-your-opey-install}/
162162
```
163163

164+
# Building the frontend container
165+
166+
As the frontend environment is read at build time, we need to reprocess the values
167+
at container runtime.
168+
This is done here: Dockerfiles/prestart.go
169+
overwriting the values set here: Dockerfiles/frontend_build.env
170+
Any newly introduced environment variables should be added to the prestart.go and frontend_build.env files accordingly.
164171
# LICENSE
165172

166173
This project is licensed under the AGPL V3 (see NOTICE) and a commercial license from TESOBE.

components.d.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ export {}
77
/* prettier-ignore */
88
declare module 'vue' {
99
export interface GlobalComponents {
10+
ChatMessage: typeof import('./src/components/ChatMessage.vue')['default']
1011
ChatWidget: typeof import('./src/components/ChatWidget.vue')['default']
11-
ChatWidgetII: typeof import('./src/components/ChatWidgetII.vue')['default']
12+
ChatWidgetOld: typeof import('./src/components/ChatWidgetOld.vue')['default']
1213
Collections: typeof import('./src/components/Collections.vue')['default']
1314
Content: typeof import('./src/components/Content.vue')['default']
1415
ElAlert: typeof import('element-plus/es')['ElAlert']
@@ -32,9 +33,8 @@ declare module 'vue' {
3233
ElIcon: typeof import('element-plus/es')['ElIcon']
3334
ElInput: typeof import('element-plus/es')['ElInput']
3435
ElMain: typeof import('element-plus/es')['ElMain']
35-
ElMenu: typeof import('element-plus/es')['ElMenu']
36-
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
3736
ElRow: typeof import('element-plus/es')['ElRow']
37+
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
3838
ElTooltip: typeof import('element-plus/es')['ElTooltip']
3939
GlossarySearchNav: typeof import('./src/components/GlossarySearchNav.vue')['default']
4040
HeaderNav: typeof import('./src/components/HeaderNav.vue')['default']
@@ -45,7 +45,4 @@ declare module 'vue' {
4545
RouterView: typeof import('vue-router')['RouterView']
4646
SearchNav: typeof import('./src/components/SearchNav.vue')['default']
4747
}
48-
export interface ComponentCustomProperties {
49-
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
50-
}
5148
}

package.json

+12-4
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
"build": "run-p build-only",
1111
"build-server": "tsc --project tsconfig.server.json",
1212
"preview": "vite preview",
13-
"test:unit": "vitest",
14-
"test": "jest --silent=false",
13+
"test": "vitest",
1514
"build-only": "vite build",
1615
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
1716
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
@@ -35,9 +34,11 @@
3534
"highlight.js": "^11.8.0",
3635
"json-editor-vue": "^0.17.3",
3736
"jsonwebtoken": "^9.0.2",
37+
"langchain": "^0.3.19",
3838
"markdown-it": "^14.1.0",
3939
"node-fetch": "^2.6.7",
4040
"oauth": "^0.10.0",
41+
"obp-api-typescript": "^1.0.1",
4142
"obp-typescript": "^1.0.36",
4243
"pinia": "^2.0.37",
4344
"prismjs": "^1.29.0",
@@ -56,20 +57,24 @@
5657
"ws": "^8.18.0"
5758
},
5859
"devDependencies": {
60+
"@ai-sdk/vue": "^1.1.18",
5961
"@rushstack/eslint-patch": "^1.4.0",
62+
"@testing-library/vue": "^8.1.0",
6063
"@types/jest": "^29.5.14",
6164
"@types/jsdom": "^21.1.7",
6265
"@types/jsonwebtoken": "^9.0.6",
6366
"@types/markdown-it": "^14.1.1",
6467
"@types/node": "^20.5.7",
65-
"@vitejs/plugin-vue": "^4.3.0",
68+
"@types/oauth": "^0.9.6",
69+
"@vitejs/plugin-vue": "^4.6.2",
6670
"@vitejs/plugin-vue-jsx": "^3.1.0",
6771
"@vue/eslint-config-prettier": "^9.0.0",
6872
"@vue/eslint-config-typescript": "^14.0.0",
6973
"@vue/test-utils": "^2.4.0",
7074
"@vue/tsconfig": "^0.1.3",
7175
"eslint": "^9.15.0",
7276
"eslint-plugin-vue": "^9.12.0",
77+
"happy-dom": "^17.1.4",
7378
"jest": "^29.7.0",
7479
"jsdom": "^25.0.1",
7580
"node-mocks-http": "^1.16.2",
@@ -86,7 +91,10 @@
8691
"vite": "^4.4.0",
8792
"vite-plugin-node-polyfills": "^0.10.0",
8893
"vite-plugin-rewrite-all": "^1.0.2",
89-
"vitest": "^0.34.0",
94+
"vitest": "^0.34.6",
9095
"vue-tsc": "^2.0.0"
96+
},
97+
"overrides": {
98+
"@langchain/core": "0.1.5"
9199
}
92100
}

server/controllers/OpeyIIController.ts

+118-25
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Controller, Session, Req, Res, Post, Get } from 'routing-controllers'
22
import { Request, Response } from 'express'
3-
import { pipeline } from "node:stream/promises"
3+
import { Transform, pipeline, Readable } from "node:stream"
4+
import { ReadableStream as WebReadableStream } from "stream/web"
45
import { Service } from 'typedi'
56
import OBPClientService from '../services/OBPClientService'
67
import OpeyClientService from '../services/OpeyClientService'
78

89
import { UserInput } from '../schema/OpeySchema'
10+
import { APIApi, Configuration, ConsentApi, ConsumerConsentrequestsBody, InlineResponse20151 } from 'obp-api-typescript'
911

1012
@Service()
1113
@Controller('/opey')
@@ -42,9 +44,11 @@ export class OpeyController {
4244
@Res() response: Response,
4345
) {
4446

47+
// Read user input from request body
4548
let user_input: UserInput
4649
try {
47-
user_input = {
50+
console.log("Request body: ", request.body)
51+
user_input = {
4852
"message": request.body.message,
4953
"thread_id": request.body.thread_id,
5054
"is_tool_call_approval": request.body.is_tool_call_approval
@@ -54,45 +58,73 @@ export class OpeyController {
5458
return response.status(500).json({ error: 'Internal Server Error' })
5559
}
5660

57-
58-
console.log("Calling OpeyClientService.stream")
5961

60-
// const streamMiddlewareTransform = new Transform({
61-
// transform(chunk, encoding, callback) {
62-
// console.log(`Logged Chunk: ${chunk}`)
63-
// this.push(chunk);
64-
65-
// callback();
66-
// }
67-
// })
62+
// Define a function to transform the response from Opey (which is a text stream) into a TS-Native langchain stream
63+
const frontendTransformer = new TransformStream({
64+
transform(chunk, controller) {
65+
// Decode the chunk to a string
66+
const decodedChunk = new TextDecoder().decode(chunk)
67+
68+
console.log("Sending chunk", decodedChunk)
69+
controller.enqueue(decodedChunk);
70+
},
71+
flush(controller) {
72+
console.log('[flush]');
73+
// Close ReadableStream when done
74+
controller.terminate();
75+
},
76+
});
77+
6878

69-
let stream: NodeJS.ReadableStream | null = null
79+
let stream: ReadableStream | null = null
7080

7181
try {
72-
// Read stream from OpeyClientService
82+
// Read web stream from OpeyClientService
83+
console.log("Calling OpeyClientService.stream")
7384
stream = await this.opeyClientService.stream(user_input)
74-
console.debug(`Stream received readable: ${stream?.readable}`)
7585

7686
} catch (error) {
7787
console.error("Error reading stream: ", error)
7888
return response.status(500).json({ error: 'Internal Server Error' })
7989
}
8090

81-
if (!stream || !stream.readable) {
91+
if (!stream) {
8292
console.error("Stream is not recieved or not readable")
8393
return response.status(500).json({ error: 'Internal Server Error' })
8494
}
8595

96+
97+
// Transform our stream if needed, right now this is just a passthrough
98+
const frontendStream: ReadableStream = stream.pipeThrough(frontendTransformer)
99+
100+
// If we need to split the stream into two, we can use the tee method as below
101+
102+
// const streamTee = langchainStream.tee()
103+
// if (!streamTee) {
104+
// console.error("Stream is not tee'd")
105+
// return response.status(500).json({ error: 'Internal Server Error' })
106+
// }
107+
// const [stream1, stream2] = streamTee
108+
109+
110+
111+
const nodeStream = Readable.fromWeb(frontendStream as WebReadableStream<any>)
112+
113+
response.setHeader('x-vercel-ai-data-stream', 'v1')
114+
response.setHeader('Content-Type', 'text/event-stream');
115+
response.setHeader('Cache-Control', 'no-cache');
116+
response.setHeader('Connection', 'keep-alive');
117+
nodeStream.pipe(response);
118+
119+
86120
return new Promise<Response>((resolve, reject) => {
87-
stream.pipe(response)
88-
stream.on('end', () => {
89-
response.status(200)
90-
resolve(response)
91-
})
92-
stream.on('error', (error) => {
93-
console.error("Error piping stream: ", error)
94-
reject(error)
95-
})
121+
nodeStream.on('end', () => {
122+
resolve(response);
123+
});
124+
nodeStream.on('error', (error) => {
125+
console.error('Stream error:', error);
126+
reject(error);
127+
});
96128

97129
})
98130

@@ -129,6 +161,67 @@ export class OpeyController {
129161
}
130162
}
131163

164+
@Post('/consent/request')
165+
/**
166+
* Retrieves a consent request from OBP
167+
*
168+
*/
169+
async getConsentRequest(
170+
@Session() session: any,
171+
@Req() request: Request,
172+
@Res() response: Response,
173+
): Promise<Response | any> {
174+
try {
175+
176+
let obpToken: string
177+
178+
obpToken = await this.obpClientService.getDirectLoginToken()
179+
console.log("Got token: ", obpToken)
180+
const authHeader = `DirectLogin token="${obpToken}"`
181+
console.log("Auth header: ", authHeader)
182+
183+
const obpOAuthHeaders = await this.obpClientService.getOAuthHeader('/consents', 'POST')
184+
console.log("OBP OAuth Headers: ", obpOAuthHeaders)
185+
186+
const obpConfig: Configuration = {
187+
apiKey: authHeader,
188+
basePath: process.env.VITE_OBP_API_HOST,
189+
}
190+
191+
console.log("OBP Config: ", obpConfig)
192+
193+
const consentAPI = new ConsentApi(obpConfig, process.env.VITE_OBP_API_HOST)
194+
195+
196+
// OBP sdk naming is a bit mad, can be rectified in the future
197+
const consentRequestResponse = await consentAPI.oBPv500CreateConsentRequest({
198+
accountAccess: [],
199+
everything: false,
200+
entitlements: [],
201+
consumerId: '',
202+
} as unknown as ConsumerConsentrequestsBody,
203+
{
204+
headers: {
205+
'Content-Type': 'application/json',
206+
},
207+
}
208+
)
209+
210+
//console.log("Consent request response: ", consentRequestResponse)
211+
212+
console.log({consentId: consentRequestResponse.data.consent_request_id})
213+
session['obpConsentRequestId'] = consentRequestResponse.data.consent_request_id
214+
215+
return response.status(200).json(JSON.stringify({consentId: consentRequestResponse.data.consent_request_id}))
216+
//console.log(await response.body.json())
217+
218+
219+
} catch (error) {
220+
console.error("Error in consent/request endpoint: ", error);
221+
return response.status(500).json({ error: 'Internal Server Error' });
222+
}
223+
}
224+
132225
@Post('/consent')
133226
/**
134227
* Retrieves a consent from OBP for the current user

server/schema/OpeySchema.ts

+4
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@ export type OpeyConfig = {
2222
export type AuthConfig = {
2323
consentId: string,
2424
opeyJWT: string,
25+
}
26+
27+
export interface ConsentRequestResponse {
28+
consentId: string;
2529
}

0 commit comments

Comments
 (0)