Skip to content

Commit d990dc8

Browse files
authored
feat: add no-ai filtering support for my feed (#3766)
1 parent d3c35ba commit d990dc8

5 files changed

Lines changed: 265 additions & 8 deletions

File tree

__tests__/feeds.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,8 +671,8 @@ describe('query feed', () => {
671671
};
672672

673673
const QUERY = `
674-
query Feed($ranking: Ranking, $first: Int, $after: String, $version: Int, $unreadOnly: Boolean, $supportedTypes: [String!]) {
675-
feed(ranking: $ranking, first: $first, after: $after, version: $version, unreadOnly: $unreadOnly, supportedTypes: $supportedTypes) {
674+
query Feed($ranking: Ranking, $first: Int, $after: String, $version: Int, $unreadOnly: Boolean, $supportedTypes: [String!], $noAi: Boolean) {
675+
feed(ranking: $ranking, first: $first, after: $after, version: $version, unreadOnly: $unreadOnly, supportedTypes: $supportedTypes, noAi: $noAi) {
676676
${feedFields()}
677677
}
678678
}
@@ -689,6 +689,45 @@ describe('query feed', () => {
689689
expect(res.data).toMatchSnapshot();
690690
});
691691

692+
it('should include no-ai blocked tags and title words in the v2 feed config', async () => {
693+
loggedUser = '1';
694+
await saveFeedFixtures();
695+
nock('http://localhost:6002')
696+
.post('/config')
697+
.reply(200, {
698+
user_id: '1',
699+
config: {
700+
providers: {},
701+
},
702+
});
703+
nock('http://localhost:6000')
704+
.post('/feed.json', (body) => {
705+
expect(body.blocked_tags).toEqual(
706+
expect.arrayContaining(['golang', 'ai', 'openai']),
707+
);
708+
expect(body.blocked_title_words).toEqual(
709+
expect.arrayContaining(['Claude', 'Elon Musk']),
710+
);
711+
712+
return true;
713+
})
714+
.reply(200, {
715+
data: [{ post_id: 'p1' }, { post_id: 'p4' }],
716+
cursor: 'b',
717+
});
718+
719+
const res = await client.query(QUERY, {
720+
variables: {
721+
...variables,
722+
noAi: true,
723+
version: 20,
724+
},
725+
});
726+
727+
expect(res.errors).toBeFalsy();
728+
expect(res.data.feed.edges.length).toEqual(2);
729+
});
730+
692731
describe('youtube content', () => {
693732
beforeEach(async () => {
694733
await saveFixtures(con, YouTubePost, videoPostsFixture);

src/common/feedGenerator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@ export const anonymousFeedBuilder = (
705705
whereNotTags(filters.blockedTags!, builder, alias),
706706
);
707707
}
708+
708709
return newBuilder;
709710
};
710711

src/common/noAiFilter.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
export const NO_AI_BLOCKED_TAGS = [
2+
'ai',
3+
'machine-learning',
4+
'data-science',
5+
'llm',
6+
'deep-learning',
7+
'nlp',
8+
'genai',
9+
'openai',
10+
'chatgpt',
11+
'computer-vision',
12+
'ai-agents',
13+
'neural-networks',
14+
'pytorch',
15+
'tensorflow',
16+
'rag',
17+
'reinforcement-learning',
18+
'transformers',
19+
'prompt-engineering',
20+
'claude',
21+
'gpt',
22+
'vector-search',
23+
'anthropic',
24+
'google-gemini',
25+
'conversational-ai',
26+
'langchain',
27+
'mlops',
28+
'ai-coding',
29+
'ai-safety',
30+
'ai-security',
31+
'embeddings',
32+
'scikit',
33+
'aiops',
34+
'ai-assisted-development',
35+
'keras',
36+
'prompt-injection',
37+
'claude-code',
38+
'llama',
39+
'responsible-ai',
40+
'agentic-ai',
41+
'stable-diffusion',
42+
'huggingface',
43+
'vertex-ai',
44+
'ollama',
45+
'unsupervised-learning',
46+
'bert',
47+
'text-to-speech',
48+
'ai-governance',
49+
'ai-infrastructure',
50+
'text-generation',
51+
'midjourney',
52+
'vibe-coding',
53+
'diffusion-models',
54+
'deepseek',
55+
'mistral-ai',
56+
'grok',
57+
'explainable-ai',
58+
'ai-regulation',
59+
'google-ai',
60+
'vllm',
61+
'spring-ai',
62+
'ai-inference',
63+
'voice-ai',
64+
'stability-ai',
65+
'perplexity',
66+
'weaviate',
67+
'brain-computer-interface',
68+
'microsoft-copilot',
69+
'generative-art',
70+
'azure-openai',
71+
'dalle',
72+
'cohere',
73+
'edge-ai',
74+
'sora',
75+
'amazon-q',
76+
'llmops',
77+
'ai-engineering',
78+
'pinecone',
79+
'gemma',
80+
'local-ai',
81+
'azure-ai',
82+
'windsurf',
83+
'gpt-store',
84+
'codewhisperer',
85+
'duet-ai',
86+
] as const;
87+
88+
export const NO_AI_BLOCKED_WORDS = [
89+
'ChatGPT',
90+
'GPT',
91+
'GPT-4',
92+
'GPT-5',
93+
'OpenAI',
94+
'Claude',
95+
'Claude Code',
96+
'Anthropic',
97+
'Gemini',
98+
'Copilot',
99+
'DeepSeek',
100+
'Mistral',
101+
'LLaMA',
102+
'Llama',
103+
'Ollama',
104+
'Qwen',
105+
'Mixtral',
106+
'Groq',
107+
'Cohere',
108+
'Codex',
109+
'DALL-E',
110+
'Sora',
111+
'Midjourney',
112+
'Stable Diffusion',
113+
'Gemma',
114+
'MiniMax',
115+
'Cursor',
116+
'Windsurf',
117+
'Devin',
118+
'Tabnine',
119+
'Codeium',
120+
'Supermaven',
121+
'LangChain',
122+
'LangSmith',
123+
'HuggingFace',
124+
'Hugging Face',
125+
'vLLM',
126+
'Perplexity',
127+
'Pinecone',
128+
'Weaviate',
129+
'LLM',
130+
'GenAI',
131+
'AGI',
132+
'NLP',
133+
'RAG',
134+
'AI agent',
135+
'AI agents',
136+
'agentic',
137+
'vibe coding',
138+
'vibe-coding',
139+
'machine learning',
140+
'deep learning',
141+
'neural network',
142+
'fine-tuning',
143+
'fine-tune',
144+
'embeddings',
145+
'transformer',
146+
'diffusion model',
147+
'prompt engineering',
148+
'prompt injection',
149+
'inference',
150+
'AI coding',
151+
'AI code',
152+
'AI-generated',
153+
'AI-powered',
154+
'AI-driven',
155+
'AI-assisted',
156+
'AI tool',
157+
'AI tools',
158+
'AI model',
159+
'AI models',
160+
'AI safety',
161+
'AI security',
162+
'AI governance',
163+
'AI regulation',
164+
'Dario Amodei',
165+
'Sam Altman',
166+
'Jensen Huang',
167+
'Elon Musk',
168+
'xAI',
169+
] as const;

src/integrations/feed/generators.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
baseFeedConfig,
33
DynamicConfig,
44
FeedConfigGenerator,
5+
FeedConfigGeneratorResult,
56
FeedConfigName,
67
FeedResponse,
78
FeedVersion,
@@ -47,6 +48,23 @@ export class FeedGenerator {
4748
extraMetadata,
4849
);
4950
}
51+
52+
withConfigTransform(
53+
transform: (
54+
result: FeedConfigGeneratorResult,
55+
) => FeedConfigGeneratorResult | Promise<FeedConfigGeneratorResult>,
56+
): FeedGenerator {
57+
const baseConfig = this.config;
58+
59+
return new FeedGenerator(
60+
this.client,
61+
{
62+
generate: async (ctx, opts) =>
63+
transform(await baseConfig.generate(ctx, opts)),
64+
},
65+
this.feedId,
66+
);
67+
}
5068
}
5169

5270
const garmFeedService = new GarmrService({

src/schema/feeds.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import { randomUUID } from 'crypto';
8787
import { SourceMemberRoles } from '../roles';
8888
import { ContentPreferenceKeyword } from '../entity/contentPreference/ContentPreferenceKeyword';
8989
import { briefingPostIdsMaxItems } from '../common/brief';
90+
import { NO_AI_BLOCKED_TAGS, NO_AI_BLOCKED_WORDS } from '../common/noAiFilter';
9091

9192
interface GQLTagsCategory {
9293
id: string;
@@ -394,6 +395,11 @@ export const typeDefs = /* GraphQL */ `
394395
Array of supported post types
395396
"""
396397
supportedTypes: [String!]
398+
399+
"""
400+
Exclude AI-related content from the feed
401+
"""
402+
noAi: Boolean = false
397403
): PostConnection! @auth
398404
399405
"""
@@ -1120,6 +1126,7 @@ interface AnonymousFeedArgs extends FeedArgs {
11201126
interface ConfiguredFeedArgs extends FeedArgs {
11211127
unreadOnly: boolean;
11221128
version: number;
1129+
noAi?: boolean;
11231130
}
11241131

11251132
interface SourceFeedArgs extends FeedArgs {
@@ -1364,6 +1371,27 @@ const anonymousFeedResolverV1: IFieldResolver<
13641371
{ allowPrivatePosts: false },
13651372
);
13661373

1374+
const mergeUniqueStrings = (
1375+
current: string[] | undefined,
1376+
additions: readonly string[],
1377+
): string[] => Array.from(new Set([...(current ?? []), ...additions]));
1378+
1379+
const wrapGeneratorWithNoAi = (generator: FeedGenerator): FeedGenerator =>
1380+
generator.withConfigTransform((result) => ({
1381+
...result,
1382+
config: {
1383+
...result.config,
1384+
blocked_tags: mergeUniqueStrings(
1385+
result.config.blocked_tags,
1386+
NO_AI_BLOCKED_TAGS,
1387+
),
1388+
blocked_title_words: mergeUniqueStrings(
1389+
result.config.blocked_title_words,
1390+
NO_AI_BLOCKED_WORDS,
1391+
),
1392+
},
1393+
}));
1394+
13671395
const feedResolverV1: IFieldResolver<unknown, Context, ConfiguredFeedArgs> =
13681396
feedResolver(
13691397
(
@@ -1387,10 +1415,8 @@ const feedResolverV1: IFieldResolver<unknown, Context, ConfiguredFeedArgs> =
13871415
feedPageGenerator,
13881416
applyFeedPaging,
13891417
{
1390-
fetchQueryParams: async (ctx, args) => {
1391-
const feedId = args.feedId || ctx.userId;
1392-
return feedToFilters(ctx.con, feedId, ctx.userId);
1393-
},
1418+
fetchQueryParams: async (ctx, args) =>
1419+
feedToFilters(ctx.con, args.feedId || ctx.userId, ctx.userId),
13941420
allowPrivatePosts: false,
13951421
},
13961422
);
@@ -1567,22 +1593,26 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
15671593
},
15681594
feed: (source, args: ConfiguredFeedArgs, ctx: Context, info) => {
15691595
if (args.version >= 2 && args.ranking === Ranking.POPULARITY) {
1596+
const generator = versionToFeedGenerator(args.version);
1597+
15701598
return feedResolverCursor(
15711599
source,
15721600
{
15731601
...(args as FeedArgs),
1574-
generator: versionToFeedGenerator(args.version),
1602+
generator: args.noAi ? wrapGeneratorWithNoAi(generator) : generator,
15751603
},
15761604
ctx,
15771605
info,
15781606
);
15791607
}
15801608
if (args.version >= 2 && args.ranking === Ranking.TIME) {
1609+
const generator = versionToTimeFeedGenerator(args.version);
1610+
15811611
return feedResolverCursor(
15821612
source,
15831613
{
15841614
...(args as FeedArgs),
1585-
generator: versionToTimeFeedGenerator(args.version),
1615+
generator: args.noAi ? wrapGeneratorWithNoAi(generator) : generator,
15861616
},
15871617
ctx,
15881618
info,

0 commit comments

Comments
 (0)