gingdev
commited on
Commit
•
cfc0cce
0
Parent(s):
first commit
Browse files- .editorconfig +12 -0
- .gitignore +2 -0
- README.md +14 -0
- app/__init__.py +0 -0
- app/dependencies.py +0 -0
- app/internal/__init__.py +0 -0
- app/internal/constants.py +3 -0
- app/main.py +21 -0
- app/routers/__init__.py +0 -0
- app/routers/duckduckgo.py +109 -0
- main.py +5 -0
- requirements.txt +7 -0
.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
|