gingdev commited on
Commit
cfc0cce
0 Parent(s):

first commit

Browse files
.editorconfig ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ [*]
7
+ indent_style = space
8
+ indent_size = 4
9
+ end_of_line = lf
10
+ charset = utf-8
11
+ trim_trailing_whitespace = true
12
+ insert_final_newline = true
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .venv
2
+ __pycache__
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Freedom AI
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ pip install -r requirements.txt
7
+ ```
8
+
9
+ ## Usage
10
+ ```bash
11
+ python main.py
12
+ ```
13
+
14
+ Visit http://localhost:8000
app/__init__.py ADDED
File without changes
app/dependencies.py ADDED
File without changes
app/internal/__init__.py ADDED
File without changes
app/internal/constants.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ DUCKDUCKGO_CHAT_ENDPOINT = 'https://duckduckgo.com/duckchat/v1/chat'
2
+ DUCKDUCKGO_STATUS_ENDPOINT = 'https://duckduckgo.com/duckchat/v1/status'
3
+ DEFAULT_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
app/main.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from typing import AsyncIterator, TypedDict
3
+ from fastapi import FastAPI
4
+ import httpx
5
+ from .routers import duckduckgo
6
+
7
+ class State(TypedDict):
8
+ http_client: httpx.AsyncClient
9
+
10
+ @asynccontextmanager
11
+ async def lifespan(app: FastAPI) -> AsyncIterator[State]:
12
+ async with httpx.AsyncClient() as http_client:
13
+ yield {'http_client': http_client}
14
+
15
+ app = FastAPI(title='Freedom LLM', description='Free AI for everyone', lifespan=lifespan)
16
+
17
+ app.include_router(duckduckgo.router)
18
+
19
+ @app.get('/')
20
+ async def root():
21
+ return {'message': 'Hello, my name is Ging'}
app/routers/__init__.py ADDED
File without changes
app/routers/duckduckgo.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+
3
+ from fastapi import APIRouter, HTTPException, Header, Request, Response
4
+ from httpx_sse import EventSource
5
+ from sse_starlette.sse import EventSourceResponse
6
+ from starlette.background import BackgroundTask
7
+ from app.internal.constants import DEFAULT_USER_AGENT, DUCKDUCKGO_CHAT_ENDPOINT, DUCKDUCKGO_STATUS_ENDPOINT
8
+ from typing import Annotated, List, Literal, cast
9
+ from pydantic import BaseModel
10
+
11
+ DONE = '[DONE]'
12
+
13
+
14
+ class Message(BaseModel):
15
+ role: Literal['assistant', 'user']
16
+ content: str
17
+
18
+
19
+ class Chat(BaseModel):
20
+ model: Literal[
21
+ 'gpt-3.5-turbo-0125',
22
+ 'claude-3-haiku-20240307'
23
+ ] = 'gpt-3.5-turbo-0125'
24
+ messages: list[Message]
25
+ stream: bool = False
26
+
27
+ model_config = {
28
+ 'json_schema_extra': {
29
+ 'examples': [
30
+ {
31
+ 'model': 'claude-3-haiku-20240307',
32
+ 'messages': [{
33
+ 'role': 'user',
34
+ 'content': 'Hello',
35
+ }]
36
+ }
37
+ ]
38
+ }
39
+ }
40
+
41
+
42
+ class Choice(BaseModel):
43
+ message: Message
44
+
45
+
46
+ class CompletionsResult(BaseModel):
47
+ choices: List[Choice]
48
+
49
+
50
+ router = APIRouter()
51
+
52
+
53
+ @router.post('/ddg/chat/completions',
54
+ response_model=CompletionsResult,
55
+ responses={
56
+ 200: {
57
+ 'content': {'text/event-stream': {}},
58
+ 'description': 'Return the JSON completions result or an event stream.',
59
+ }
60
+ })
61
+ async def chat(input: Chat, request: Request, response: Response, x_session_id: Annotated[str | None, Header()] = None):
62
+ http_client: httpx.AsyncClient = request.state.http_client
63
+ session_id = x_session_id or (await http_client.get(DUCKDUCKGO_STATUS_ENDPOINT, headers={
64
+ 'x-vqd-accept': '1',
65
+ 'user-agent': DEFAULT_USER_AGENT,
66
+ })).headers.get('x-vqd-4')
67
+
68
+ req = http_client.build_request('POST', DUCKDUCKGO_CHAT_ENDPOINT,
69
+ json=input.model_dump(exclude={'stream'}),
70
+ headers={
71
+ 'x-vqd-4': session_id,
72
+ 'user-agent': DEFAULT_USER_AGENT
73
+ })
74
+ resp = await http_client.send(req, stream=input.stream)
75
+
76
+ if resp.status_code != 200:
77
+ raise HTTPException(status_code=400)
78
+
79
+ async def agenerator():
80
+ async for event in EventSource(resp).aiter_sse():
81
+ if event.data == DONE:
82
+ return
83
+
84
+ content = cast(dict[str, str], event.json()).get('message')
85
+ if content:
86
+ yield content
87
+
88
+ async def event_generator():
89
+ async for chunk in agenerator():
90
+ yield {
91
+ 'data': {
92
+ 'choices': [{
93
+ 'delta': chunk
94
+ }]
95
+ }
96
+ }
97
+
98
+ yield DONE
99
+
100
+ response.headers['x-session-id'] = resp.headers.get('x-vqd-4')
101
+
102
+ if input.stream:
103
+ return EventSourceResponse(event_generator(), background=BackgroundTask(resp.aclose), headers=response.headers)
104
+
105
+ content = ''
106
+ async for chunk in agenerator():
107
+ content += chunk
108
+
109
+ return CompletionsResult(choices=[Choice(message=Message(role='assistant', content=content))])
main.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import uvicorn
2
+ from app.main import app
3
+
4
+ if __name__ == '__main__':
5
+ uvicorn.run(app)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ httpx==0.27.0
3
+ httpx_sse==0.4.0
4
+ pydantic==2.7.1
5
+ sse_starlette==2.1.0
6
+ starlette==0.37.2
7
+ uvicorn==0.29.0